One Hat Cyber Team
Your IP:
216.73.216.102
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
home
/
fluxyjvi
/
www
/
assets
/
images
/
Edit File:
src.tar
Queue.php 0000644 00000007115 15105646576 0006364 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Exception\NoSuchElementException; use Ramsey\Collection\Tool\TypeTrait; use Ramsey\Collection\Tool\ValueToStringTrait; use function array_key_first; /** * This class provides a basic implementation of `QueueInterface`, to minimize * the effort required to implement this interface. * * @template T * @extends AbstractArray<T> * @implements QueueInterface<T> */ class Queue extends AbstractArray implements QueueInterface { use TypeTrait; use ValueToStringTrait; /** * Constructs a queue object of the specified type, optionally with the * specified data. * * @param string $queueType The type or class name associated with this queue. * @param array<array-key, T> $data The initial items to store in the queue. */ public function __construct(private readonly string $queueType, array $data = []) { parent::__construct($data); } /** * {@inheritDoc} * * Since arbitrary offsets may not be manipulated in a queue, this method * serves only to fulfill the `ArrayAccess` interface requirements. It is * invoked by other operations when adding values to the queue. * * @throws InvalidArgumentException if $value is of the wrong type. */ public function offsetSet(mixed $offset, mixed $value): void { if ($this->checkType($this->getType(), $value) === false) { throw new InvalidArgumentException( 'Value must be of type ' . $this->getType() . '; value is ' . $this->toolValueToString($value), ); } $this->data[] = $value; } /** * @throws InvalidArgumentException if $value is of the wrong type. */ public function add(mixed $element): bool { $this[] = $element; return true; } /** * @return T * * @throws NoSuchElementException if this queue is empty. */ public function element(): mixed { return $this->peek() ?? throw new NoSuchElementException( 'Can\'t return element from Queue. Queue is empty.', ); } public function offer(mixed $element): bool { try { return $this->add($element); } catch (InvalidArgumentException) { return false; } } /** * @return T | null */ public function peek(): mixed { $index = array_key_first($this->data); if ($index === null) { return null; } return $this[$index]; } /** * @return T | null */ public function poll(): mixed { $index = array_key_first($this->data); if ($index === null) { return null; } $head = $this[$index]; unset($this[$index]); return $head; } /** * @return T * * @throws NoSuchElementException if this queue is empty. */ public function remove(): mixed { return $this->poll() ?? throw new NoSuchElementException( 'Can\'t return element from Queue. Queue is empty.', ); } public function getType(): string { return $this->queueType; } } CollectionInterface.php 0000644 00000021706 15105646576 0011216 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Ramsey\Collection\Exception\CollectionMismatchException; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Exception\InvalidPropertyOrMethod; use Ramsey\Collection\Exception\NoSuchElementException; use Ramsey\Collection\Exception\UnsupportedOperationException; /** * A collection represents a group of values, known as its elements. * * Some collections allow duplicate elements and others do not. Some are ordered * and others unordered. * * @template T * @extends ArrayInterface<T> */ interface CollectionInterface extends ArrayInterface { /** * Ensures that this collection contains the specified element (optional * operation). * * Returns `true` if this collection changed as a result of the call. * (Returns `false` if this collection does not permit duplicates and * already contains the specified element.) * * Collections that support this operation may place limitations on what * elements may be added to this collection. In particular, some * collections will refuse to add `null` elements, and others will impose * restrictions on the type of elements that may be added. Collection * classes should clearly specify in their documentation any restrictions * on what elements may be added. * * If a collection refuses to add a particular element for any reason other * than that it already contains the element, it must throw an exception * (rather than returning `false`). This preserves the invariant that a * collection always contains the specified element after this call returns. * * @param T $element The element to add to the collection. * * @return bool `true` if this collection changed as a result of the call. * * @throws InvalidArgumentException if the collection refuses to add the * $element for any reason other than that it already contains the element. */ public function add(mixed $element): bool; /** * Returns `true` if this collection contains the specified element. * * @param T $element The element to check whether the collection contains. * @param bool $strict Whether to perform a strict type check on the value. */ public function contains(mixed $element, bool $strict = true): bool; /** * Returns the type associated with this collection. */ public function getType(): string; /** * Removes a single instance of the specified element from this collection, * if it is present. * * @param T $element The element to remove from the collection. * * @return bool `true` if an element was removed as a result of this call. */ public function remove(mixed $element): bool; /** * Returns the values from the given property, method, or array key. * * @param string $propertyOrMethod The name of the property, method, or * array key to evaluate and return. * * @return array<int, mixed> * * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call column() on this * collection. */ public function column(string $propertyOrMethod): array; /** * Returns the first item of the collection. * * @return T * * @throws NoSuchElementException if this collection is empty. */ public function first(): mixed; /** * Returns the last item of the collection. * * @return T * * @throws NoSuchElementException if this collection is empty. */ public function last(): mixed; /** * Sort the collection by a property, method, or array key with the given * sort order. * * If $propertyOrMethod is `null`, this will sort by comparing each element. * * This will always leave the original collection untouched and will return * a new one. * * @param string | null $propertyOrMethod The property, method, or array key * to sort by. * @param Sort $order The sort order for the resulting collection. * * @return CollectionInterface<T> * * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call sort() on this * collection. */ public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): self; /** * Filter out items of the collection which don't match the criteria of * given callback. * * This will always leave the original collection untouched and will return * a new one. * * See the {@link http://php.net/manual/en/function.array-filter.php PHP array_filter() documentation} * for examples of how the `$callback` parameter works. * * @param callable(T): bool $callback A callable to use for filtering elements. * * @return CollectionInterface<T> */ public function filter(callable $callback): self; /** * Create a new collection where the result of the given property, method, * or array key of each item in the collection equals the given value. * * This will always leave the original collection untouched and will return * a new one. * * @param string | null $propertyOrMethod The property, method, or array key * to evaluate. If `null`, the element itself is compared to $value. * @param mixed $value The value to match. * * @return CollectionInterface<T> * * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call where() on this * collection. */ public function where(?string $propertyOrMethod, mixed $value): self; /** * Apply a given callback method on each item of the collection. * * This will always leave the original collection untouched. The new * collection is created by mapping the callback to each item of the * original collection. * * See the {@link http://php.net/manual/en/function.array-map.php PHP array_map() documentation} * for examples of how the `$callback` parameter works. * * @param callable(T): TCallbackReturn $callback A callable to apply to each * item of the collection. * * @return CollectionInterface<TCallbackReturn> * * @template TCallbackReturn */ public function map(callable $callback): self; /** * Apply a given callback method on each item of the collection * to reduce it to a single value. * * See the {@link http://php.net/manual/en/function.array-reduce.php PHP array_reduce() documentation} * for examples of how the `$callback` and `$initial` parameters work. * * @param callable(TCarry, T): TCarry $callback A callable to apply to each * item of the collection to reduce it to a single value. * @param TCarry $initial This is the initial value provided to the callback. * * @return TCarry * * @template TCarry */ public function reduce(callable $callback, mixed $initial): mixed; /** * Create a new collection with divergent items between current and given * collection. * * @param CollectionInterface<T> $other The collection to check for divergent * items. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if the compared collections are of * differing types. */ public function diff(CollectionInterface $other): self; /** * Create a new collection with intersecting item between current and given * collection. * * @param CollectionInterface<T> $other The collection to check for * intersecting items. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if the compared collections are of * differing types. */ public function intersect(CollectionInterface $other): self; /** * Merge current items and items of given collections into a new one. * * @param CollectionInterface<T> ...$collections The collections to merge. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if unable to merge any of the given * collections or items within the given collections due to type * mismatch errors. */ public function merge(CollectionInterface ...$collections): self; } DoubleEndedQueue.php 0000644 00000010300 15105646576 0010445 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Exception\NoSuchElementException; use function array_key_last; use function array_pop; use function array_unshift; /** * This class provides a basic implementation of `DoubleEndedQueueInterface`, to * minimize the effort required to implement this interface. * * @template T * @extends Queue<T> * @implements DoubleEndedQueueInterface<T> */ class DoubleEndedQueue extends Queue implements DoubleEndedQueueInterface { /** * Constructs a double-ended queue (dequeue) object of the specified type, * optionally with the specified data. * * @param string $queueType The type or class name associated with this dequeue. * @param array<array-key, T> $data The initial items to store in the dequeue. */ public function __construct(private readonly string $queueType, array $data = []) { parent::__construct($this->queueType, $data); } /** * @throws InvalidArgumentException if $element is of the wrong type */ public function addFirst(mixed $element): bool { if ($this->checkType($this->getType(), $element) === false) { throw new InvalidArgumentException( 'Value must be of type ' . $this->getType() . '; value is ' . $this->toolValueToString($element), ); } array_unshift($this->data, $element); return true; } /** * @throws InvalidArgumentException if $element is of the wrong type */ public function addLast(mixed $element): bool { return $this->add($element); } public function offerFirst(mixed $element): bool { try { return $this->addFirst($element); } catch (InvalidArgumentException) { return false; } } public function offerLast(mixed $element): bool { return $this->offer($element); } /** * @return T the first element in this queue. * * @throws NoSuchElementException if the queue is empty */ public function removeFirst(): mixed { return $this->remove(); } /** * @return T the last element in this queue. * * @throws NoSuchElementException if this queue is empty. */ public function removeLast(): mixed { return $this->pollLast() ?? throw new NoSuchElementException( 'Can\'t return element from Queue. Queue is empty.', ); } /** * @return T | null the head of this queue, or `null` if this queue is empty. */ public function pollFirst(): mixed { return $this->poll(); } /** * @return T | null the tail of this queue, or `null` if this queue is empty. */ public function pollLast(): mixed { return array_pop($this->data); } /** * @return T the head of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function firstElement(): mixed { return $this->element(); } /** * @return T the tail of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function lastElement(): mixed { return $this->peekLast() ?? throw new NoSuchElementException( 'Can\'t return element from Queue. Queue is empty.', ); } /** * @return T | null the head of this queue, or `null` if this queue is empty. */ public function peekFirst(): mixed { return $this->peek(); } /** * @return T | null the tail of this queue, or `null` if this queue is empty. */ public function peekLast(): mixed { $lastIndex = array_key_last($this->data); if ($lastIndex === null) { return null; } return $this->data[$lastIndex]; } } AbstractCollection.php 0000644 00000027507 15105646576 0011066 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Closure; use Ramsey\Collection\Exception\CollectionMismatchException; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Exception\InvalidPropertyOrMethod; use Ramsey\Collection\Exception\NoSuchElementException; use Ramsey\Collection\Exception\UnsupportedOperationException; use Ramsey\Collection\Tool\TypeTrait; use Ramsey\Collection\Tool\ValueExtractorTrait; use Ramsey\Collection\Tool\ValueToStringTrait; use function array_filter; use function array_key_first; use function array_key_last; use function array_map; use function array_merge; use function array_reduce; use function array_search; use function array_udiff; use function array_uintersect; use function in_array; use function is_int; use function is_object; use function spl_object_id; use function sprintf; use function usort; /** * This class provides a basic implementation of `CollectionInterface`, to * minimize the effort required to implement this interface * * @template T * @extends AbstractArray<T> * @implements CollectionInterface<T> */ abstract class AbstractCollection extends AbstractArray implements CollectionInterface { use TypeTrait; use ValueToStringTrait; use ValueExtractorTrait; /** * @throws InvalidArgumentException if $element is of the wrong type. */ public function add(mixed $element): bool { $this[] = $element; return true; } public function contains(mixed $element, bool $strict = true): bool { return in_array($element, $this->data, $strict); } /** * @throws InvalidArgumentException if $element is of the wrong type. */ public function offsetSet(mixed $offset, mixed $value): void { if ($this->checkType($this->getType(), $value) === false) { throw new InvalidArgumentException( 'Value must be of type ' . $this->getType() . '; value is ' . $this->toolValueToString($value), ); } if ($offset === null) { $this->data[] = $value; } else { $this->data[$offset] = $value; } } public function remove(mixed $element): bool { if (($position = array_search($element, $this->data, true)) !== false) { unset($this[$position]); return true; } return false; } /** * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call column() on this * collection. * * @inheritDoc */ public function column(string $propertyOrMethod): array { $temp = []; foreach ($this->data as $item) { /** @psalm-suppress MixedAssignment */ $temp[] = $this->extractValue($item, $propertyOrMethod); } return $temp; } /** * @return T * * @throws NoSuchElementException if this collection is empty. */ public function first(): mixed { $firstIndex = array_key_first($this->data); if ($firstIndex === null) { throw new NoSuchElementException('Can\'t determine first item. Collection is empty'); } return $this->data[$firstIndex]; } /** * @return T * * @throws NoSuchElementException if this collection is empty. */ public function last(): mixed { $lastIndex = array_key_last($this->data); if ($lastIndex === null) { throw new NoSuchElementException('Can\'t determine last item. Collection is empty'); } return $this->data[$lastIndex]; } /** * @return CollectionInterface<T> * * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call sort() on this * collection. */ public function sort(?string $propertyOrMethod = null, Sort $order = Sort::Ascending): CollectionInterface { $collection = clone $this; usort( $collection->data, /** * @param T $a * @param T $b */ function (mixed $a, mixed $b) use ($propertyOrMethod, $order): int { /** @var mixed $aValue */ $aValue = $this->extractValue($a, $propertyOrMethod); /** @var mixed $bValue */ $bValue = $this->extractValue($b, $propertyOrMethod); return ($aValue <=> $bValue) * ($order === Sort::Descending ? -1 : 1); }, ); return $collection; } /** * @param callable(T): bool $callback A callable to use for filtering elements. * * @return CollectionInterface<T> */ public function filter(callable $callback): CollectionInterface { $collection = clone $this; $collection->data = array_merge([], array_filter($collection->data, $callback)); return $collection; } /** * @return CollectionInterface<T> * * @throws InvalidPropertyOrMethod if the $propertyOrMethod does not exist * on the elements in this collection. * @throws UnsupportedOperationException if unable to call where() on this * collection. */ public function where(?string $propertyOrMethod, mixed $value): CollectionInterface { return $this->filter( /** * @param T $item */ function (mixed $item) use ($propertyOrMethod, $value): bool { /** @var mixed $accessorValue */ $accessorValue = $this->extractValue($item, $propertyOrMethod); return $accessorValue === $value; }, ); } /** * @param callable(T): TCallbackReturn $callback A callable to apply to each * item of the collection. * * @return CollectionInterface<TCallbackReturn> * * @template TCallbackReturn */ public function map(callable $callback): CollectionInterface { /** @var Collection<TCallbackReturn> */ return new Collection('mixed', array_map($callback, $this->data)); } /** * @param callable(TCarry, T): TCarry $callback A callable to apply to each * item of the collection to reduce it to a single value. * @param TCarry $initial This is the initial value provided to the callback. * * @return TCarry * * @template TCarry */ public function reduce(callable $callback, mixed $initial): mixed { /** @var TCarry */ return array_reduce($this->data, $callback, $initial); } /** * @param CollectionInterface<T> $other The collection to check for divergent * items. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if the compared collections are of * differing types. */ public function diff(CollectionInterface $other): CollectionInterface { $this->compareCollectionTypes($other); $diffAtoB = array_udiff($this->data, $other->toArray(), $this->getComparator()); $diffBtoA = array_udiff($other->toArray(), $this->data, $this->getComparator()); /** @var array<array-key, T> $diff */ $diff = array_merge($diffAtoB, $diffBtoA); $collection = clone $this; $collection->data = $diff; return $collection; } /** * @param CollectionInterface<T> $other The collection to check for * intersecting items. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if the compared collections are of * differing types. */ public function intersect(CollectionInterface $other): CollectionInterface { $this->compareCollectionTypes($other); /** @var array<array-key, T> $intersect */ $intersect = array_uintersect($this->data, $other->toArray(), $this->getComparator()); $collection = clone $this; $collection->data = $intersect; return $collection; } /** * @param CollectionInterface<T> ...$collections The collections to merge. * * @return CollectionInterface<T> * * @throws CollectionMismatchException if unable to merge any of the given * collections or items within the given collections due to type * mismatch errors. */ public function merge(CollectionInterface ...$collections): CollectionInterface { $mergedCollection = clone $this; foreach ($collections as $index => $collection) { if (!$collection instanceof static) { throw new CollectionMismatchException( sprintf('Collection with index %d must be of type %s', $index, static::class), ); } // When using generics (Collection.php, Set.php, etc), // we also need to make sure that the internal types match each other if ($this->getUniformType($collection) !== $this->getUniformType($this)) { throw new CollectionMismatchException( sprintf( 'Collection items in collection with index %d must be of type %s', $index, $this->getType(), ), ); } foreach ($collection as $key => $value) { if (is_int($key)) { $mergedCollection[] = $value; } else { $mergedCollection[$key] = $value; } } } return $mergedCollection; } /** * @param CollectionInterface<T> $other * * @throws CollectionMismatchException */ private function compareCollectionTypes(CollectionInterface $other): void { if (!$other instanceof static) { throw new CollectionMismatchException('Collection must be of type ' . static::class); } // When using generics (Collection.php, Set.php, etc), // we also need to make sure that the internal types match each other if ($this->getUniformType($other) !== $this->getUniformType($this)) { throw new CollectionMismatchException('Collection items must be of type ' . $this->getType()); } } private function getComparator(): Closure { return /** * @param T $a * @param T $b */ function (mixed $a, mixed $b): int { // If the two values are object, we convert them to unique scalars. // If the collection contains mixed values (unlikely) where some are objects // and some are not, we leave them as they are. // The comparator should still work and the result of $a < $b should // be consistent but unpredictable since not documented. if (is_object($a) && is_object($b)) { $a = spl_object_id($a); $b = spl_object_id($b); } return $a === $b ? 0 : ($a < $b ? 1 : -1); }; } /** * @param CollectionInterface<mixed> $collection */ private function getUniformType(CollectionInterface $collection): string { return match ($collection->getType()) { 'integer' => 'int', 'boolean' => 'bool', 'double' => 'float', default => $collection->getType(), }; } } AbstractArray.php 0000644 00000010424 15105646576 0010037 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use ArrayIterator; use Traversable; use function count; /** * This class provides a basic implementation of `ArrayInterface`, to minimize * the effort required to implement this interface. * * @template T * @implements ArrayInterface<T> */ abstract class AbstractArray implements ArrayInterface { /** * The items of this array. * * @var array<array-key, T> */ protected array $data = []; /** * Constructs a new array object. * * @param array<array-key, T> $data The initial items to add to this array. */ public function __construct(array $data = []) { // Invoke offsetSet() for each value added; in this way, sub-classes // may provide additional logic about values added to the array object. foreach ($data as $key => $value) { $this[$key] = $value; } } /** * Returns an iterator for this array. * * @link http://php.net/manual/en/iteratoraggregate.getiterator.php IteratorAggregate::getIterator() * * @return Traversable<array-key, T> */ public function getIterator(): Traversable { return new ArrayIterator($this->data); } /** * Returns `true` if the given offset exists in this array. * * @link http://php.net/manual/en/arrayaccess.offsetexists.php ArrayAccess::offsetExists() * * @param array-key $offset The offset to check. */ public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); } /** * Returns the value at the specified offset. * * @link http://php.net/manual/en/arrayaccess.offsetget.php ArrayAccess::offsetGet() * * @param array-key $offset The offset for which a value should be returned. * * @return T the value stored at the offset, or null if the offset * does not exist. */ public function offsetGet(mixed $offset): mixed { return $this->data[$offset]; } /** * Sets the given value to the given offset in the array. * * @link http://php.net/manual/en/arrayaccess.offsetset.php ArrayAccess::offsetSet() * * @param array-key | null $offset The offset to set. If `null`, the value * may be set at a numerically-indexed offset. * @param T $value The value to set at the given offset. */ public function offsetSet(mixed $offset, mixed $value): void { if ($offset === null) { $this->data[] = $value; } else { $this->data[$offset] = $value; } } /** * Removes the given offset and its value from the array. * * @link http://php.net/manual/en/arrayaccess.offsetunset.php ArrayAccess::offsetUnset() * * @param array-key $offset The offset to remove from the array. */ public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); } /** * Returns data suitable for PHP serialization. * * @link https://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.serialize * @link https://www.php.net/serialize * * @return array<array-key, T> */ public function __serialize(): array { return $this->data; } /** * Adds unserialized data to the object. * * @param array<array-key, T> $data */ public function __unserialize(array $data): void { $this->data = $data; } /** * Returns the number of items in this array. * * @link http://php.net/manual/en/countable.count.php Countable::count() */ public function count(): int { return count($this->data); } public function clear(): void { $this->data = []; } /** * @inheritDoc */ public function toArray(): array { return $this->data; } public function isEmpty(): bool { return $this->data === []; } } Tool/TypeTrait.php 0000644 00000003065 15105646576 0010142 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Tool; use function is_array; use function is_bool; use function is_callable; use function is_float; use function is_int; use function is_numeric; use function is_object; use function is_resource; use function is_scalar; use function is_string; /** * Provides functionality to check values for specific types. */ trait TypeTrait { /** * Returns `true` if value is of the specified type. * * @param string $type The type to check the value against. * @param mixed $value The value to check. */ protected function checkType(string $type, mixed $value): bool { return match ($type) { 'array' => is_array($value), 'bool', 'boolean' => is_bool($value), 'callable' => is_callable($value), 'float', 'double' => is_float($value), 'int', 'integer' => is_int($value), 'null' => $value === null, 'numeric' => is_numeric($value), 'object' => is_object($value), 'resource' => is_resource($value), 'scalar' => is_scalar($value), 'string' => is_string($value), 'mixed' => true, default => $value instanceof $type, }; } } Tool/ValueToStringTrait.php 0000644 00000004361 15105646576 0011767 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Tool; use DateTimeInterface; use function assert; use function get_resource_type; use function is_array; use function is_bool; use function is_callable; use function is_object; use function is_resource; use function is_scalar; /** * Provides functionality to express a value as string */ trait ValueToStringTrait { /** * Returns a string representation of the value. * * - null value: `'NULL'` * - boolean: `'TRUE'`, `'FALSE'` * - array: `'Array'` * - scalar: converted-value * - resource: `'(type resource #number)'` * - object with `__toString()`: result of `__toString()` * - object DateTime: ISO 8601 date * - object: `'(className Object)'` * - anonymous function: same as object * * @param mixed $value the value to return as a string. */ protected function toolValueToString(mixed $value): string { // null if ($value === null) { return 'NULL'; } // boolean constants if (is_bool($value)) { return $value ? 'TRUE' : 'FALSE'; } // array if (is_array($value)) { return 'Array'; } // scalar types (integer, float, string) if (is_scalar($value)) { return (string) $value; } // resource if (is_resource($value)) { return '(' . get_resource_type($value) . ' resource #' . (int) $value . ')'; } // From here, $value should be an object. assert(is_object($value)); // __toString() is implemented if (is_callable([$value, '__toString'])) { return (string) $value->__toString(); } // object of type \DateTime if ($value instanceof DateTimeInterface) { return $value->format('c'); } // unknown type return '(' . $value::class . ' Object)'; } } Tool/ValueExtractorTrait.php 0000644 00000004671 15105646576 0012175 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Tool; use Ramsey\Collection\Exception\InvalidPropertyOrMethod; use Ramsey\Collection\Exception\UnsupportedOperationException; use function is_array; use function is_object; use function method_exists; use function property_exists; use function sprintf; /** * Provides functionality to extract the value of a property or method from an object. */ trait ValueExtractorTrait { /** * Extracts the value of the given property, method, or array key from the * element. * * If `$propertyOrMethod` is `null`, we return the element as-is. * * @param mixed $element The element to extract the value from. * @param string | null $propertyOrMethod The property or method for which the * value should be extracted. * * @return mixed the value extracted from the specified property, method, * or array key, or the element itself. * * @throws InvalidPropertyOrMethod * @throws UnsupportedOperationException */ protected function extractValue(mixed $element, ?string $propertyOrMethod): mixed { if ($propertyOrMethod === null) { return $element; } if (!is_object($element) && !is_array($element)) { throw new UnsupportedOperationException(sprintf( 'The collection type "%s" does not support the $propertyOrMethod parameter', $this->getType(), )); } if (is_array($element)) { return $element[$propertyOrMethod] ?? throw new InvalidPropertyOrMethod(sprintf( 'Key or index "%s" not found in collection elements', $propertyOrMethod, )); } if (property_exists($element, $propertyOrMethod)) { return $element->$propertyOrMethod; } if (method_exists($element, $propertyOrMethod)) { return $element->{$propertyOrMethod}(); } throw new InvalidPropertyOrMethod(sprintf( 'Method or property "%s" not defined in %s', $propertyOrMethod, $element::class, )); } } ArrayInterface.php 0000644 00000002045 15105646576 0010174 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use ArrayAccess; use Countable; use IteratorAggregate; /** * `ArrayInterface` provides traversable array functionality to data types. * * @template T * @extends ArrayAccess<array-key, T> * @extends IteratorAggregate<array-key, T> */ interface ArrayInterface extends ArrayAccess, Countable, IteratorAggregate { /** * Removes all items from this array. */ public function clear(): void; /** * Returns a native PHP array representation of this array object. * * @return array<array-key, T> */ public function toArray(): array; /** * Returns `true` if this array is empty. */ public function isEmpty(): bool; } DoubleEndedQueueInterface.php 0000644 00000024171 15105646576 0012301 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Ramsey\Collection\Exception\NoSuchElementException; use RuntimeException; /** * A linear collection that supports element insertion and removal at both ends. * * Most `DoubleEndedQueueInterface` implementations place no fixed limits on the * number of elements they may contain, but this interface supports * capacity-restricted double-ended queues as well as those with no fixed size * limit. * * This interface defines methods to access the elements at both ends of the * double-ended queue. Methods are provided to insert, remove, and examine the * element. Each of these methods exists in two forms: one throws an exception * if the operation fails, the other returns a special value (either `null` or * `false`, depending on the operation). The latter form of the insert operation * is designed specifically for use with capacity-restricted implementations; in * most implementations, insert operations cannot fail. * * The twelve methods described above are summarized in the following table: * * <table> * <caption>Summary of DoubleEndedQueueInterface methods</caption> * <thead> * <tr> * <th></th> * <th colspan=2>First Element (Head)</th> * <th colspan=2>Last Element (Tail)</th> * </tr> * <tr> * <td></td> * <td><em>Throws exception</em></td> * <td><em>Special value</em></td> * <td><em>Throws exception</em></td> * <td><em>Special value</em></td> * </tr> * </thead> * <tbody> * <tr> * <th>Insert</th> * <td><code>addFirst()</code></td> * <td><code>offerFirst()</code></td> * <td><code>addLast()</code></td> * <td><code>offerLast()</code></td> * </tr> * <tr> * <th>Remove</th> * <td><code>removeFirst()</code></td> * <td><code>pollFirst()</code></td> * <td><code>removeLast()</code></td> * <td><code>pollLast()</code></td> * </tr> * <tr> * <th>Examine</th> * <td><code>firstElement()</code></td> * <td><code>peekFirst()</code></td> * <td><code>lastElement()</code></td> * <td><code>peekLast()</code></td> * </tr> * </tbody> * </table> * * This interface extends the `QueueInterface`. When a double-ended queue is * used as a queue, FIFO (first-in-first-out) behavior results. Elements are * added at the end of the double-ended queue and removed from the beginning. * The methods inherited from the `QueueInterface` are precisely equivalent to * `DoubleEndedQueueInterface` methods as indicated in the following table: * * <table> * <caption>Comparison of QueueInterface and DoubleEndedQueueInterface methods</caption> * <thead> * <tr> * <th>QueueInterface Method</th> * <th>DoubleEndedQueueInterface Method</th> * </tr> * </thead> * <tbody> * <tr> * <td><code>add()</code></td> * <td><code>addLast()</code></td> * </tr> * <tr> * <td><code>offer()</code></td> * <td><code>offerLast()</code></td> * </tr> * <tr> * <td><code>remove()</code></td> * <td><code>removeFirst()</code></td> * </tr> * <tr> * <td><code>poll()</code></td> * <td><code>pollFirst()</code></td> * </tr> * <tr> * <td><code>element()</code></td> * <td><code>firstElement()</code></td> * </tr> * <tr> * <td><code>peek()</code></td> * <td><code>peekFirst()</code></td> * </tr> * </tbody> * </table> * * Double-ended queues can also be used as LIFO (last-in-first-out) stacks. When * a double-ended queue is used as a stack, elements are pushed and popped from * the beginning of the double-ended queue. Stack concepts are precisely * equivalent to `DoubleEndedQueueInterface` methods as indicated in the table * below: * * <table> * <caption>Comparison of stack concepts and DoubleEndedQueueInterface methods</caption> * <thead> * <tr> * <th>Stack concept</th> * <th>DoubleEndedQueueInterface Method</th> * </tr> * </thead> * <tbody> * <tr> * <td><em>push</em></td> * <td><code>addFirst()</code></td> * </tr> * <tr> * <td><em>pop</em></td> * <td><code>removeFirst()</code></td> * </tr> * <tr> * <td><em>peek</em></td> * <td><code>peekFirst()</code></td> * </tr> * </tbody> * </table> * * Note that the `peek()` method works equally well when a double-ended queue is * used as a queue or a stack; in either case, elements are drawn from the * beginning of the double-ended queue. * * While `DoubleEndedQueueInterface` implementations are not strictly required * to prohibit the insertion of `null` elements, they are strongly encouraged to * do so. Users of any `DoubleEndedQueueInterface` implementations that do allow * `null` elements are strongly encouraged *not* to take advantage of the * ability to insert nulls. This is so because `null` is used as a special * return value by various methods to indicated that the double-ended queue is * empty. * * @template T * @extends QueueInterface<T> */ interface DoubleEndedQueueInterface extends QueueInterface { /** * Inserts the specified element at the front of this queue if it is * possible to do so immediately without violating capacity restrictions. * * When using a capacity-restricted double-ended queue, it is generally * preferable to use the `offerFirst()` method. * * @param T $element The element to add to the front of this queue. * * @return bool `true` if this queue changed as a result of the call. * * @throws RuntimeException if a queue refuses to add a particular element * for any reason other than that it already contains the element. * Implementations should use a more-specific exception that extends * `\RuntimeException`. */ public function addFirst(mixed $element): bool; /** * Inserts the specified element at the end of this queue if it is possible * to do so immediately without violating capacity restrictions. * * When using a capacity-restricted double-ended queue, it is generally * preferable to use the `offerLast()` method. * * This method is equivalent to `add()`. * * @param T $element The element to add to the end of this queue. * * @return bool `true` if this queue changed as a result of the call. * * @throws RuntimeException if a queue refuses to add a particular element * for any reason other than that it already contains the element. * Implementations should use a more-specific exception that extends * `\RuntimeException`. */ public function addLast(mixed $element): bool; /** * Inserts the specified element at the front of this queue if it is * possible to do so immediately without violating capacity restrictions. * * When using a capacity-restricted queue, this method is generally * preferable to `addFirst()`, which can fail to insert an element only by * throwing an exception. * * @param T $element The element to add to the front of this queue. * * @return bool `true` if the element was added to this queue, else `false`. */ public function offerFirst(mixed $element): bool; /** * Inserts the specified element at the end of this queue if it is possible * to do so immediately without violating capacity restrictions. * * When using a capacity-restricted queue, this method is generally * preferable to `addLast()` which can fail to insert an element only by * throwing an exception. * * @param T $element The element to add to the end of this queue. * * @return bool `true` if the element was added to this queue, else `false`. */ public function offerLast(mixed $element): bool; /** * Retrieves and removes the head of this queue. * * This method differs from `pollFirst()` only in that it throws an * exception if this queue is empty. * * @return T the first element in this queue. * * @throws NoSuchElementException if this queue is empty. */ public function removeFirst(): mixed; /** * Retrieves and removes the tail of this queue. * * This method differs from `pollLast()` only in that it throws an exception * if this queue is empty. * * @return T the last element in this queue. * * @throws NoSuchElementException if this queue is empty. */ public function removeLast(): mixed; /** * Retrieves and removes the head of this queue, or returns `null` if this * queue is empty. * * @return T | null the head of this queue, or `null` if this queue is empty. */ public function pollFirst(): mixed; /** * Retrieves and removes the tail of this queue, or returns `null` if this * queue is empty. * * @return T | null the tail of this queue, or `null` if this queue is empty. */ public function pollLast(): mixed; /** * Retrieves, but does not remove, the head of this queue. * * This method differs from `peekFirst()` only in that it throws an * exception if this queue is empty. * * @return T the head of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function firstElement(): mixed; /** * Retrieves, but does not remove, the tail of this queue. * * This method differs from `peekLast()` only in that it throws an exception * if this queue is empty. * * @return T the tail of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function lastElement(): mixed; /** * Retrieves, but does not remove, the head of this queue, or returns `null` * if this queue is empty. * * @return T | null the head of this queue, or `null` if this queue is empty. */ public function peekFirst(): mixed; /** * Retrieves, but does not remove, the tail of this queue, or returns `null` * if this queue is empty. * * @return T | null the tail of this queue, or `null` if this queue is empty. */ public function peekLast(): mixed; } Collection.php 0000644 00000004433 15105646577 0007374 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; /** * A collection represents a group of objects. * * Each object in the collection is of a specific, defined type. * * This is a direct implementation of `CollectionInterface`, provided for * the sake of convenience. * * Example usage: * * ``` php * $collection = new \Ramsey\Collection\Collection('My\\Foo'); * $collection->add(new \My\Foo()); * $collection->add(new \My\Foo()); * * foreach ($collection as $foo) { * // Do something with $foo * } * ``` * * It is preferable to subclass `AbstractCollection` to create your own typed * collections. For example: * * ``` php * namespace My\Foo; * * class FooCollection extends \Ramsey\Collection\AbstractCollection * { * public function getType() * { * return 'My\\Foo'; * } * } * ``` * * And then use it similarly to the earlier example: * * ``` php * $fooCollection = new \My\Foo\FooCollection(); * $fooCollection->add(new \My\Foo()); * $fooCollection->add(new \My\Foo()); * * foreach ($fooCollection as $foo) { * // Do something with $foo * } * ``` * * The benefit with this approach is that you may do type-checking on the * collection object: * * ``` php * if ($collection instanceof \My\Foo\FooCollection) { * // the collection is a collection of My\Foo objects * } * ``` * * @template T * @extends AbstractCollection<T> */ class Collection extends AbstractCollection { /** * Constructs a collection object of the specified type, optionally with the * specified data. * * @param string $collectionType The type or class name associated with this * collection. * @param array<array-key, T> $data The initial items to store in the collection. */ public function __construct(private readonly string $collectionType, array $data = []) { parent::__construct($data); } public function getType(): string { return $this->collectionType; } } Exception/OutOfBoundsException.php 0000644 00000001152 15105646577 0013320 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use OutOfBoundsException as PhpOutOfBoundsException; /** * Thrown when attempting to access an element out of the range of the collection. */ class OutOfBoundsException extends PhpOutOfBoundsException implements CollectionException { } Exception/NoSuchElementException.php 0000644 00000001067 15105646577 0013627 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use RuntimeException; /** * Thrown when attempting to access an element that does not exist. */ class NoSuchElementException extends RuntimeException implements CollectionException { } Exception/UnsupportedOperationException.php 0000644 00000001077 15105646577 0015330 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use RuntimeException; /** * Thrown to indicate that the requested operation is not supported. */ class UnsupportedOperationException extends RuntimeException implements CollectionException { } Exception/CollectionMismatchException.php 0000644 00000001100 15105646577 0014663 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use RuntimeException; /** * Thrown when attempting to operate on collections of differing types. */ class CollectionMismatchException extends RuntimeException implements CollectionException { } Exception/InvalidPropertyOrMethod.php 0000644 00000001233 15105646577 0014027 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use RuntimeException; /** * Thrown when attempting to evaluate a property, method, or array key * that doesn't exist on an element or cannot otherwise be evaluated in the * current context. */ class InvalidPropertyOrMethod extends RuntimeException implements CollectionException { } Exception/InvalidArgumentException.php 0000644 00000000634 15105646577 0014206 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class InvalidArgumentException extends \InvalidArgumentException implements CommonMarkException { } Exception/CollectionException.php 0000644 00000000677 15105646577 0013217 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Exception; use Throwable; interface CollectionException extends Throwable { } Set.php 0000644 00000003046 15105646577 0006033 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; /** * A set is a collection that contains no duplicate elements. * * Great care must be exercised if mutable objects are used as set elements. * The behavior of a set is not specified if the value of an object is changed * in a manner that affects equals comparisons while the object is an element in * the set. * * Example usage: * * ``` php * $foo = new \My\Foo(); * $set = new Set(\My\Foo::class); * * $set->add($foo); // returns TRUE, the element doesn't exist * $set->add($foo); // returns FALSE, the element already exists * * $bar = new \My\Foo(); * $set->add($bar); // returns TRUE, $bar !== $foo * ``` * * @template T * @extends AbstractSet<T> */ class Set extends AbstractSet { /** * Constructs a set object of the specified type, optionally with the * specified data. * * @param string $setType The type or class name associated with this set. * @param array<array-key, T> $data The initial items to store in the set. */ public function __construct(private readonly string $setType, array $data = []) { parent::__construct($data); } public function getType(): string { return $this->setType; } } QueueInterface.php 0000644 00000016335 15105646577 0010212 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; use Ramsey\Collection\Exception\NoSuchElementException; use RuntimeException; /** * A queue is a collection in which the entities in the collection are kept in * order. * * The principal operations on the queue are the addition of entities to the end * (tail), also known as *enqueue*, and removal of entities from the front * (head), also known as *dequeue*. This makes the queue a first-in-first-out * (FIFO) data structure. * * Besides basic array operations, queues provide additional insertion, * extraction, and inspection operations. Each of these methods exists in two * forms: one throws an exception if the operation fails, the other returns a * special value (either `null` or `false`, depending on the operation). The * latter form of the insert operation is designed specifically for use with * capacity-restricted `QueueInterface` implementations; in most * implementations, insert operations cannot fail. * * <table> * <caption>Summary of QueueInterface methods</caption> * <thead> * <tr> * <td></td> * <td><em>Throws exception</em></td> * <td><em>Returns special value</em></td> * </tr> * </thead> * <tbody> * <tr> * <th>Insert</th> * <td><code>add()</code></td> * <td><code>offer()</code></td> * </tr> * <tr> * <th>Remove</th> * <td><code>remove()</code></td> * <td><code>poll()</code></td> * </tr> * <tr> * <th>Examine</th> * <td><code>element()</code></td> * <td><code>peek()</code></td> * </tr> * </tbody> * </table> * * Queues typically, but do not necessarily, order elements in a FIFO * (first-in-first-out) manner. Among the exceptions are priority queues, which * order elements according to a supplied comparator, or the elements' natural * ordering, and LIFO queues (or stacks) which order the elements LIFO * (last-in-first-out). Whatever the ordering used, the head of the queue is * that element which would be removed by a call to remove() or poll(). In a * FIFO queue, all new elements are inserted at the tail of the queue. Other * kinds of queues may use different placement rules. Every `QueueInterface` * implementation must specify its ordering properties. * * The `offer()` method inserts an element if possible, otherwise returning * `false`. This differs from the `add()` method, which can fail to add an * element only by throwing an unchecked exception. The `offer()` method is * designed for use when failure is a normal, rather than exceptional * occurrence, for example, in fixed-capacity (or "bounded") queues. * * The `remove()` and `poll()` methods remove and return the head of the queue. * Exactly which element is removed from the queue is a function of the queue's * ordering policy, which differs from implementation to implementation. The * `remove()` and `poll()` methods differ only in their behavior when the queue * is empty: the `remove()` method throws an exception, while the `poll()` * method returns `null`. * * The `element()` and `peek()` methods return, but do not remove, the head of * the queue. * * `QueueInterface` implementations generally do not allow insertion of `null` * elements, although some implementations do not prohibit insertion of `null`. * Even in the implementations that permit it, `null` should not be inserted * into a queue, as `null` is also used as a special return value by the * `poll()` method to indicate that the queue contains no elements. * * @template T * @extends ArrayInterface<T> */ interface QueueInterface extends ArrayInterface { /** * Ensures that this queue contains the specified element (optional * operation). * * Returns `true` if this queue changed as a result of the call. (Returns * `false` if this queue does not permit duplicates and already contains the * specified element.) * * Queues that support this operation may place limitations on what elements * may be added to this queue. In particular, some queues will refuse to add * `null` elements, and others will impose restrictions on the type of * elements that may be added. Queue classes should clearly specify in their * documentation any restrictions on what elements may be added. * * If a queue refuses to add a particular element for any reason other than * that it already contains the element, it must throw an exception (rather * than returning `false`). This preserves the invariant that a queue always * contains the specified element after this call returns. * * @see self::offer() * * @param T $element The element to add to this queue. * * @return bool `true` if this queue changed as a result of the call. * * @throws RuntimeException if a queue refuses to add a particular element * for any reason other than that it already contains the element. * Implementations should use a more-specific exception that extends * `\RuntimeException`. */ public function add(mixed $element): bool; /** * Retrieves, but does not remove, the head of this queue. * * This method differs from `peek()` only in that it throws an exception if * this queue is empty. * * @see self::peek() * * @return T the head of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function element(): mixed; /** * Inserts the specified element into this queue if it is possible to do so * immediately without violating capacity restrictions. * * When using a capacity-restricted queue, this method is generally * preferable to `add()`, which can fail to insert an element only by * throwing an exception. * * @see self::add() * * @param T $element The element to add to this queue. * * @return bool `true` if the element was added to this queue, else `false`. */ public function offer(mixed $element): bool; /** * Retrieves, but does not remove, the head of this queue, or returns `null` * if this queue is empty. * * @see self::element() * * @return T | null the head of this queue, or `null` if this queue is empty. */ public function peek(): mixed; /** * Retrieves and removes the head of this queue, or returns `null` * if this queue is empty. * * @see self::remove() * * @return T | null the head of this queue, or `null` if this queue is empty. */ public function poll(): mixed; /** * Retrieves and removes the head of this queue. * * This method differs from `poll()` only in that it throws an exception if * this queue is empty. * * @see self::poll() * * @return T the head of this queue. * * @throws NoSuchElementException if this queue is empty. */ public function remove(): mixed; /** * Returns the type associated with this queue. */ public function getType(): string; } Map/MapInterface.php 0000644 00000010723 15105646577 0010353 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; use Ramsey\Collection\ArrayInterface; /** * An object that maps keys to values. * * A map cannot contain duplicate keys; each key can map to at most one value. * * @template K of array-key * @template T * @extends ArrayInterface<T> */ interface MapInterface extends ArrayInterface { /** * Returns `true` if this map contains a mapping for the specified key. * * @param K $key The key to check in the map. */ public function containsKey(int | string $key): bool; /** * Returns `true` if this map maps one or more keys to the specified value. * * This performs a strict type check on the value. * * @param T $value The value to check in the map. */ public function containsValue(mixed $value): bool; /** * Return an array of the keys contained in this map. * * @return list<K> */ public function keys(): array; /** * Returns the value to which the specified key is mapped, `null` if this * map contains no mapping for the key, or (optionally) `$defaultValue` if * this map contains no mapping for the key. * * @param K $key The key to return from the map. * @param T | null $defaultValue The default value to use if `$key` is not found. * * @return T | null the value or `null` if the key could not be found. */ public function get(int | string $key, mixed $defaultValue = null): mixed; /** * Associates the specified value with the specified key in this map. * * If the map previously contained a mapping for the key, the old value is * replaced by the specified value. * * @param K $key The key to put or replace in the map. * @param T $value The value to store at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function put(int | string $key, mixed $value): mixed; /** * Associates the specified value with the specified key in this map only if * it is not already set. * * If there is already a value associated with `$key`, this returns that * value without replacing it. * * @param K $key The key to put in the map. * @param T $value The value to store at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function putIfAbsent(int | string $key, mixed $value): mixed; /** * Removes the mapping for a key from this map if it is present. * * @param K $key The key to remove from the map. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function remove(int | string $key): mixed; /** * Removes the entry for the specified key only if it is currently mapped to * the specified value. * * This performs a strict type check on the value. * * @param K $key The key to remove from the map. * @param T $value The value to match. * * @return bool true if the value was removed. */ public function removeIf(int | string $key, mixed $value): bool; /** * Replaces the entry for the specified key only if it is currently mapped * to some value. * * @param K $key The key to replace. * @param T $value The value to set at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function replace(int | string $key, mixed $value): mixed; /** * Replaces the entry for the specified key only if currently mapped to the * specified value. * * This performs a strict type check on the value. * * @param K $key The key to remove from the map. * @param T $oldValue The value to match. * @param T $newValue The value to use as a replacement. * * @return bool true if the value was replaced. */ public function replaceIf(int | string $key, mixed $oldValue, mixed $newValue): bool; } Map/AssociativeArrayMap.php 0000644 00000001042 15105646577 0011716 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; /** * `AssociativeArrayMap` represents a standard associative array object. * * @extends AbstractMap<string, mixed> */ class AssociativeArrayMap extends AbstractMap { } Map/NamedParameterMap.php 0000644 00000006072 15105646577 0011342 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Tool\TypeTrait; use Ramsey\Collection\Tool\ValueToStringTrait; use function array_combine; use function array_key_exists; use function is_int; /** * `NamedParameterMap` represents a mapping of values to a set of named keys * that may optionally be typed * * @extends AbstractMap<string, mixed> */ class NamedParameterMap extends AbstractMap { use TypeTrait; use ValueToStringTrait; /** * Named parameters defined for this map. * * @var array<string, string> */ private readonly array $namedParameters; /** * Constructs a new `NamedParameterMap`. * * @param array<array-key, string> $namedParameters The named parameters defined for this map. * @param array<string, mixed> $data An initial set of data to set on this map. */ public function __construct(array $namedParameters, array $data = []) { $this->namedParameters = $this->filterNamedParameters($namedParameters); parent::__construct($data); } /** * Returns named parameters set for this `NamedParameterMap`. * * @return array<string, string> */ public function getNamedParameters(): array { return $this->namedParameters; } public function offsetSet(mixed $offset, mixed $value): void { if (!array_key_exists($offset, $this->namedParameters)) { throw new InvalidArgumentException( 'Attempting to set value for unconfigured parameter \'' . $this->toolValueToString($offset) . '\'', ); } if ($this->checkType($this->namedParameters[$offset], $value) === false) { throw new InvalidArgumentException( 'Value for \'' . $offset . '\' must be of type ' . $this->namedParameters[$offset] . '; value is ' . $this->toolValueToString($value), ); } $this->data[$offset] = $value; } /** * Given an array of named parameters, constructs a proper mapping of * named parameters to types. * * @param array<array-key, string> $namedParameters The named parameters to filter. * * @return array<string, string> */ protected function filterNamedParameters(array $namedParameters): array { $names = []; $types = []; foreach ($namedParameters as $key => $value) { if (is_int($key)) { $names[] = $value; $types[] = 'mixed'; } else { $names[] = $key; $types[] = $value; } } return array_combine($names, $types) ?: []; } } Map/AbstractTypedMap.php 0000644 00000003277 15105646577 0011232 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; use Ramsey\Collection\Exception\InvalidArgumentException; use Ramsey\Collection\Tool\TypeTrait; use Ramsey\Collection\Tool\ValueToStringTrait; /** * This class provides a basic implementation of `TypedMapInterface`, to * minimize the effort required to implement this interface. * * @template K of array-key * @template T * @extends AbstractMap<K, T> * @implements TypedMapInterface<K, T> */ abstract class AbstractTypedMap extends AbstractMap implements TypedMapInterface { use TypeTrait; use ValueToStringTrait; /** * @param K $offset * @param T $value * * @inheritDoc * @psalm-suppress MoreSpecificImplementedParamType */ public function offsetSet(mixed $offset, mixed $value): void { if ($this->checkType($this->getKeyType(), $offset) === false) { throw new InvalidArgumentException( 'Key must be of type ' . $this->getKeyType() . '; key is ' . $this->toolValueToString($offset), ); } if ($this->checkType($this->getValueType(), $value) === false) { throw new InvalidArgumentException( 'Value must be of type ' . $this->getValueType() . '; value is ' . $this->toolValueToString($value), ); } parent::offsetSet($offset, $value); } } Map/AbstractMap.php 0000644 00000011674 15105646577 0010224 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; use Ramsey\Collection\AbstractArray; use Ramsey\Collection\Exception\InvalidArgumentException; use Traversable; use function array_key_exists; use function array_keys; use function in_array; use function var_export; /** * This class provides a basic implementation of `MapInterface`, to minimize the * effort required to implement this interface. * * @template K of array-key * @template T * @extends AbstractArray<T> * @implements MapInterface<K, T> */ abstract class AbstractMap extends AbstractArray implements MapInterface { /** * @param array<K, T> $data The initial items to add to this map. */ public function __construct(array $data = []) { parent::__construct($data); } /** * @return Traversable<K, T> */ public function getIterator(): Traversable { return parent::getIterator(); } /** * @param K $offset The offset to set * @param T $value The value to set at the given offset. * * @inheritDoc * @psalm-suppress MoreSpecificImplementedParamType,DocblockTypeContradiction */ public function offsetSet(mixed $offset, mixed $value): void { if ($offset === null) { throw new InvalidArgumentException( 'Map elements are key/value pairs; a key must be provided for ' . 'value ' . var_export($value, true), ); } $this->data[$offset] = $value; } public function containsKey(int | string $key): bool { return array_key_exists($key, $this->data); } public function containsValue(mixed $value): bool { return in_array($value, $this->data, true); } /** * @inheritDoc */ public function keys(): array { return array_keys($this->data); } /** * @param K $key The key to return from the map. * @param T | null $defaultValue The default value to use if `$key` is not found. * * @return T | null the value or `null` if the key could not be found. */ public function get(int | string $key, mixed $defaultValue = null): mixed { return $this[$key] ?? $defaultValue; } /** * @param K $key The key to put or replace in the map. * @param T $value The value to store at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function put(int | string $key, mixed $value): mixed { $previousValue = $this->get($key); $this[$key] = $value; return $previousValue; } /** * @param K $key The key to put in the map. * @param T $value The value to store at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function putIfAbsent(int | string $key, mixed $value): mixed { $currentValue = $this->get($key); if ($currentValue === null) { $this[$key] = $value; } return $currentValue; } /** * @param K $key The key to remove from the map. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function remove(int | string $key): mixed { $previousValue = $this->get($key); unset($this[$key]); return $previousValue; } public function removeIf(int | string $key, mixed $value): bool { if ($this->get($key) === $value) { unset($this[$key]); return true; } return false; } /** * @param K $key The key to replace. * @param T $value The value to set at `$key`. * * @return T | null the previous value associated with key, or `null` if * there was no mapping for `$key`. */ public function replace(int | string $key, mixed $value): mixed { $currentValue = $this->get($key); if ($this->containsKey($key)) { $this[$key] = $value; } return $currentValue; } public function replaceIf(int | string $key, mixed $oldValue, mixed $newValue): bool { if ($this->get($key) === $oldValue) { $this[$key] = $newValue; return true; } return false; } /** * @return array<K, T> */ public function __serialize(): array { return parent::__serialize(); } /** * @return array<K, T> */ public function toArray(): array { return parent::toArray(); } } Map/TypedMapInterface.php 0000644 00000001443 15105646577 0011360 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; /** * A `TypedMapInterface` represents a map of elements where key and value are * typed. * * @template K of array-key * @template T * @extends MapInterface<K, T> */ interface TypedMapInterface extends MapInterface { /** * Return the type used on the key. */ public function getKeyType(): string; /** * Return the type forced on the values. */ public function getValueType(): string; } Map/TypedMap.php 0000644 00000005401 15105646577 0007535 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection\Map; /** * A `TypedMap` represents a map of elements where key and value are typed. * * Each element is identified by a key with defined type and a value of defined * type. The keys of the map must be unique. The values on the map can be * repeated but each with its own different key. * * The most common case is to use a string type key, but it's not limited to * this type of keys. * * This is a direct implementation of `TypedMapInterface`, provided for the sake * of convenience. * * Example usage: * * ```php * $map = new TypedMap('string', Foo::class); * $map['x'] = new Foo(); * foreach ($map as $key => $value) { * // do something with $key, it will be a Foo::class * } * * // this will throw an exception since key must be string * $map[10] = new Foo(); * * // this will throw an exception since value must be a Foo * $map['bar'] = 'bar'; * * // initialize map with contents * $map = new TypedMap('string', Foo::class, [ * new Foo(), new Foo(), new Foo() * ]); * ``` * * It is preferable to subclass `AbstractTypedMap` to create your own typed map * implementation: * * ```php * class FooTypedMap extends AbstractTypedMap * { * public function getKeyType() * { * return 'int'; * } * * public function getValueType() * { * return Foo::class; * } * } * ``` * * … but you also may use the `TypedMap` class: * * ```php * class FooTypedMap extends TypedMap * { * public function __constructor(array $data = []) * { * parent::__construct('int', Foo::class, $data); * } * } * ``` * * @template K of array-key * @template T * @extends AbstractTypedMap<K, T> */ class TypedMap extends AbstractTypedMap { /** * Constructs a map object of the specified key and value types, * optionally with the specified data. * * @param string $keyType The data type of the map's keys. * @param string $valueType The data type of the map's values. * @param array<K, T> $data The initial data to set for this map. */ public function __construct( private readonly string $keyType, private readonly string $valueType, array $data = [], ) { parent::__construct($data); } public function getKeyType(): string { return $this->keyType; } public function getValueType(): string { return $this->valueType; } } AbstractSet.php 0000644 00000002031 15105646577 0007510 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; /** * This class contains the basic implementation of a collection that does not * allow duplicated values (a set), to minimize the effort required to implement * this specific type of collection. * * @template T * @extends AbstractCollection<T> */ abstract class AbstractSet extends AbstractCollection { public function add(mixed $element): bool { if ($this->contains($element)) { return false; } return parent::add($element); } public function offsetSet(mixed $offset, mixed $value): void { if ($this->contains($value)) { return; } parent::offsetSet($offset, $value); } } Sort.php 0000644 00000001155 15105646577 0006226 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; /** * Collection sorting */ enum Sort: string { /** * Sort items in a collection in ascending order. */ case Ascending = 'asc'; /** * Sort items in a collection in descending order. */ case Descending = 'desc'; } GenericArray.php 0000644 00000001000 15105646577 0007637 0 ustar 00 <?php /** * This file is part of the ramsey/collection library * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com> * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Collection; /** * `GenericArray` represents a standard array object. * * @extends AbstractArray<mixed> */ class GenericArray extends AbstractArray { } Visitor/PreOrderVisitor.php 0000644 00000001201 15105647725 0012024 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Visitor; use Tree\Node\NodeInterface; class PreOrderVisitor implements Visitor { public function visit(NodeInterface $node) { $nodes = [ $node, ]; foreach ($node->getChildren() as $child) { $nodes = \array_merge( $nodes, $child->accept($this) ); } return $nodes; } } Visitor/Visitor.php 0000644 00000001027 15105647725 0010367 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Visitor; use Tree\Node\NodeInterface; /** * Visitor interface for Nodes. * * @author Nicolò Martini <nicmartnic@gmail.com> */ interface Visitor { /** * @param NodeInterface $node * * @return mixed */ public function visit(NodeInterface $node); } Visitor/YieldVisitor.php 0000644 00000001172 15105647725 0011357 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Visitor; use Tree\Node\NodeInterface; class YieldVisitor implements Visitor { public function visit(NodeInterface $node) { if ($node->isLeaf()) { return [$node]; } $yield = []; foreach ($node->getChildren() as $child) { $yield = \array_merge($yield, $child->accept($this)); } return $yield; } } Visitor/PostOrderVisitor.php 0000644 00000001201 15105647725 0012223 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Visitor; use Tree\Node\NodeInterface; class PostOrderVisitor implements Visitor { public function visit(NodeInterface $node) { $nodes = []; foreach ($node->getChildren() as $child) { $nodes = \array_merge( $nodes, $child->accept($this) ); } $nodes[] = $node; return $nodes; } } Builder/NodeBuilderInterface.php 0000644 00000004165 15105647725 0012702 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Builder; use Tree\Node\NodeInterface; /** * Interface that allows a fluent tree building. * * @author Nicolò Martini <nicmartnic@gmail.com> */ interface NodeBuilderInterface { /** * Set the node the builder will manage. * * @param NodeInterface $node * * @return NodeBuilderInterface The current instance */ public function setNode(NodeInterface $node); /** * Get the node the builder manages. * * @return NodeInterface */ public function getNode(); /** * Set the value of the underlaying node. * * @param mixed $value * * @return NodebuilderInterface The current instance */ public function value($value); /** * Add a leaf to the node. * * @param mixed $value The value of the leaf node * * @return NodeBuilderInterface The current instance */ public function leaf($value = null); /** * Add several leafs to the node. * * @param $value, ... An arbitrary long list of values * * @return NodeBuilderInterface The current instance */ public function leafs($value); /** * Add a child to the node enter in its scope. * * @param null $value * * @return NodeBuilderInterface A NodeBuilderInterface instance linked to the child node */ public function tree($value = null); /** * Goes up to the parent node context. * * @return null|NodeBuilderInterface A NodeBuilderInterface instanced linked to the parent node */ public function end(); /** * Return a node instance set with the given value. Implementation can follow their own logic * in choosing the NodeInterface implmentation taking into account the value. * * @param mixed $value * * @return NodeInterface */ public function nodeInstanceByValue($value = null); } Builder/NodeBuilder.php 0000644 00000004036 15105647725 0011056 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Builder; use Tree\Node\Node; use Tree\Node\NodeInterface; /** * Main implementation of the NodeBuilderInterface. */ class NodeBuilder implements NodeBuilderInterface { /** * @var NodeInterface[] */ private $nodeStack = []; public function __construct(?NodeInterface $node = null) { $this->setNode($node ?: $this->nodeInstanceByValue()); } public function setNode(NodeInterface $node) { $this ->emptyStack() ->pushNode($node); return $this; } public function getNode() { return $this->nodeStack[\count($this->nodeStack) - 1]; } public function leaf($value = null) { $this->getNode()->addChild( $this->nodeInstanceByValue($value) ); return $this; } public function leafs($value1 /*, $value2, ... */) { foreach (\func_get_args() as $value) { $this->leaf($value); } return $this; } public function tree($value = null) { $node = $this->nodeInstanceByValue($value); $this->getNode()->addChild($node); $this->pushNode($node); return $this; } public function end() { $this->popNode(); return $this; } public function nodeInstanceByValue($value = null) { return new Node($value); } public function value($value) { $this->getNode()->setValue($value); return $this; } private function emptyStack() { $this->nodeStack = []; return $this; } private function pushNode(NodeInterface $node) { \array_push($this->nodeStack, $node); return $this; } private function popNode() { return \array_pop($this->nodeStack); } } Node/Node.php 0000644 00000014502 15105647726 0007046 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; use Dflydev\DotAccessData\Data; use League\CommonMark\Exception\InvalidArgumentException; abstract class Node { /** @psalm-readonly */ public Data $data; /** @psalm-readonly-allow-private-mutation */ protected int $depth = 0; /** @psalm-readonly-allow-private-mutation */ protected ?Node $parent = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $previous = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $next = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $firstChild = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $lastChild = null; public function __construct() { $this->data = new Data([ 'attributes' => [], ]); } public function previous(): ?Node { return $this->previous; } public function next(): ?Node { return $this->next; } public function parent(): ?Node { return $this->parent; } protected function setParent(?Node $node = null): void { $this->parent = $node; $this->depth = $node === null ? 0 : $node->depth + 1; } /** * Inserts the $sibling node after $this */ public function insertAfter(Node $sibling): void { $sibling->detach(); $sibling->next = $this->next; if ($sibling->next) { $sibling->next->previous = $sibling; } $sibling->previous = $this; $this->next = $sibling; $sibling->setParent($this->parent); if (! $sibling->next && $sibling->parent) { $sibling->parent->lastChild = $sibling; } } /** * Inserts the $sibling node before $this */ public function insertBefore(Node $sibling): void { $sibling->detach(); $sibling->previous = $this->previous; if ($sibling->previous) { $sibling->previous->next = $sibling; } $sibling->next = $this; $this->previous = $sibling; $sibling->setParent($this->parent); if (! $sibling->previous && $sibling->parent) { $sibling->parent->firstChild = $sibling; } } public function replaceWith(Node $replacement): void { $replacement->detach(); $this->insertAfter($replacement); $this->detach(); } public function detach(): void { if ($this->previous) { $this->previous->next = $this->next; } elseif ($this->parent) { $this->parent->firstChild = $this->next; } if ($this->next) { $this->next->previous = $this->previous; } elseif ($this->parent) { $this->parent->lastChild = $this->previous; } $this->parent = null; $this->next = null; $this->previous = null; $this->depth = 0; } public function hasChildren(): bool { return $this->firstChild !== null; } public function firstChild(): ?Node { return $this->firstChild; } public function lastChild(): ?Node { return $this->lastChild; } /** * @return Node[] */ public function children(): iterable { $children = []; for ($current = $this->firstChild; $current !== null; $current = $current->next) { $children[] = $current; } return $children; } public function appendChild(Node $child): void { if ($this->lastChild) { $this->lastChild->insertAfter($child); } else { $child->detach(); $child->setParent($this); $this->lastChild = $this->firstChild = $child; } } /** * Adds $child as the very first child of $this */ public function prependChild(Node $child): void { if ($this->firstChild) { $this->firstChild->insertBefore($child); } else { $child->detach(); $child->setParent($this); $this->lastChild = $this->firstChild = $child; } } /** * Detaches all child nodes of given node */ public function detachChildren(): void { foreach ($this->children() as $children) { $children->setParent(null); } $this->firstChild = $this->lastChild = null; } /** * Replace all children of given node with collection of another * * @param iterable<Node> $children */ public function replaceChildren(iterable $children): void { $this->detachChildren(); foreach ($children as $item) { $this->appendChild($item); } } public function getDepth(): int { return $this->depth; } public function walker(): NodeWalker { return new NodeWalker($this); } public function iterator(int $flags = 0): NodeIterator { return new NodeIterator($this, $flags); } /** * Clone the current node and its children * * WARNING: This is a recursive function and should not be called on deeply-nested node trees! */ public function __clone() { // Cloned nodes are detached from their parents, siblings, and children $this->parent = null; $this->previous = null; $this->next = null; // But save a copy of the children since we'll need that in a moment $children = $this->children(); $this->detachChildren(); // The original children get cloned and re-added foreach ($children as $child) { $this->appendChild(clone $child); } } public static function assertInstanceOf(Node $node): void { if (! $node instanceof static) { throw new InvalidArgumentException(\sprintf('Incompatible node type: expected %s, got %s', static::class, \get_class($node))); } } } Node/NodeInterface.php 0000644 00000006271 15105647726 0010673 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Node; use Tree\Visitor\Visitor; /** * Interface for tree nodes. * * @author Nicolò Martini <nicmartnic@gmail.com> */ interface NodeInterface { /** * Set the value of the current node. * * @param mixed $value * * @return NodeInterface the current instance */ public function setValue($value); /** * Get the current node value. * * @return mixed */ public function getValue(); /** * Add a child. * * @param NodeInterface $child * * @return mixed */ public function addChild(self $child); /** * Remove a node from children. * * @param NodeInterface $child * * @return NodeInterface the current instance */ public function removeChild(self $child); /** * Remove all children. * * @return NodeInterface The current instance */ public function removeAllChildren(); /** * Return the array of children. * * @return NodeInterface[] */ public function getChildren(); /** * Replace the children set with the given one. * * @param NodeInterface[] $children * * @return mixed */ public function setChildren(array $children); /** * Set the parent node. * * @param NodeInterface $parent */ public function setParent(?self $parent = null); /** * Return the parent node. * * @return NodeInterface */ public function getParent(); /** * Retrieves all ancestors of node excluding current node. * * @return array */ public function getAncestors(); /** * Retrieves all ancestors of node as well as the node itself. * * @return Node[] */ public function getAncestorsAndSelf(); /** * Retrieves all neighboring nodes, excluding the current node. * * @return array */ public function getNeighbors(); /** * Returns all neighboring nodes, including the current node. * * @return Node[] */ public function getNeighborsAndSelf(); /** * Return true if the node is the root, false otherwise. * * @return bool */ public function isRoot(); /** * Return true if the node is a child, false otherwise. * * @return bool */ public function isChild(); /** * Return true if the node has no children, false otherwise. * * @return bool */ public function isLeaf(); /** * Return the distance from the current node to the root. * * @return int */ public function getDepth(); /** * Return the height of the tree whose root is this node. * * @return int */ public function getHeight(); /** * Accept method for the visitor pattern (see http://en.wikipedia.org/wiki/Visitor_pattern). * * @param Visitor $visitor */ public function accept(Visitor $visitor); } Node/NodeTrait.php 0000644 00000010547 15105647726 0010057 0 ustar 00 <?php /** * Copyright (c) 2013-2020 Nicolò Martini * * For the full copyright and license information, please view * the LICENSE.md file that was distributed with this source code. * * @see https://github.com/nicmart/Tree */ namespace Tree\Node; use Tree\Visitor\Visitor; trait NodeTrait { /** * @var mixed */ private $value; /** * parent. * * @var NodeInterface */ private $parent; /** * @var NodeInterface[] */ private $children = []; public function setValue($value) { $this->value = $value; return $this; } public function getValue() { return $this->value; } public function addChild(NodeInterface $child) { $child->setParent($this); $this->children[] = $child; return $this; } public function removeChild(NodeInterface $child) { foreach ($this->children as $key => $myChild) { if ($child === $myChild) { unset($this->children[$key]); } } $this->children = \array_values($this->children); $child->setParent(null); return $this; } public function removeAllChildren() { $this->setChildren([]); return $this; } public function getChildren() { return $this->children; } public function setChildren(array $children) { $this->removeParentFromChildren(); $this->children = []; foreach ($children as $child) { $this->addChild($child); } return $this; } public function setParent(?NodeInterface $parent = null) { $this->parent = $parent; } public function getParent() { return $this->parent; } public function getAncestors() { $parents = []; $node = $this; while ($parent = $node->getParent()) { \array_unshift($parents, $parent); $node = $parent; } return $parents; } public function getAncestorsAndSelf() { return \array_merge($this->getAncestors(), [$this]); } public function getNeighbors() { $neighbors = $this->getParent()->getChildren(); $current = $this; return \array_values( \array_filter( $neighbors, static function ($item) use ($current) { return $item !== $current; } ) ); } public function getNeighborsAndSelf() { return $this->getParent()->getChildren(); } public function isLeaf() { return 0 === \count($this->children); } /** * @return bool */ public function isRoot() { return null === $this->getParent(); } public function isChild() { return null !== $this->getParent(); } /** * Find the root of the node. * * @return NodeInterface */ public function root() { $node = $this; while ($parent = $node->getParent()) { $node = $parent; } return $node; } /** * Return the distance from the current node to the root. * * Warning, can be expensive, since each descendant is visited * * @return int */ public function getDepth() { if ($this->isRoot()) { return 0; } return $this->getParent()->getDepth() + 1; } /** * Return the height of the tree whose root is this node. * * @return int */ public function getHeight() { if ($this->isLeaf()) { return 0; } $heights = []; foreach ($this->getChildren() as $child) { $heights[] = $child->getHeight(); } return \max($heights) + 1; } /** * Return the number of nodes in a tree. * * @return int */ public function getSize() { $size = 1; foreach ($this->getChildren() as $child) { $size += $child->getSize(); } return $size; } public function accept(Visitor $visitor) { return $visitor->visit($this); } private function removeParentFromChildren() { foreach ($this->getChildren() as $child) { $child->setParent(null); } } } Exporter.php 0000644 00000023176 15105703425 0007100 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/exporter. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Exporter; use function bin2hex; use function count; use function get_resource_type; use function gettype; use function implode; use function ini_get; use function ini_set; use function is_array; use function is_float; use function is_object; use function is_resource; use function is_string; use function mb_strlen; use function mb_substr; use function preg_match; use function spl_object_id; use function sprintf; use function str_repeat; use function str_replace; use function var_export; use BackedEnum; use SebastianBergmann\RecursionContext\Context; use SplObjectStorage; use UnitEnum; final class Exporter { /** * Exports a value as a string. * * The output of this method is similar to the output of print_r(), but * improved in various aspects: * * - NULL is rendered as "null" (instead of "") * - TRUE is rendered as "true" (instead of "1") * - FALSE is rendered as "false" (instead of "") * - Strings are always quoted with single quotes * - Carriage returns and newlines are normalized to \n * - Recursion and repeated rendering is treated properly */ public function export(mixed $value, int $indentation = 0): string { return $this->recursiveExport($value, $indentation); } public function shortenedRecursiveExport(array &$data, Context $context = null): string { $result = []; $exporter = new self; if (!$context) { $context = new Context; } $array = $data; /* @noinspection UnusedFunctionResultInspection */ $context->add($data); foreach ($array as $key => $value) { if (is_array($value)) { if ($context->contains($data[$key]) !== false) { $result[] = '*RECURSION*'; } else { $result[] = sprintf('[%s]', $this->shortenedRecursiveExport($data[$key], $context)); } } else { $result[] = $exporter->shortenedExport($value); } } return implode(', ', $result); } /** * Exports a value into a single-line string. * * The output of this method is similar to the output of * SebastianBergmann\Exporter\Exporter::export(). * * Newlines are replaced by the visible string '\n'. * Contents of arrays and objects (if any) are replaced by '...'. */ public function shortenedExport(mixed $value): string { if (is_string($value)) { $string = str_replace("\n", '', $this->export($value)); if (mb_strlen($string) > 40) { return mb_substr($string, 0, 30) . '...' . mb_substr($string, -7); } return $string; } if ($value instanceof BackedEnum) { return sprintf( '%s Enum (%s, %s)', $value::class, $value->name, $this->export($value->value), ); } if ($value instanceof UnitEnum) { return sprintf( '%s Enum (%s)', $value::class, $value->name, ); } if (is_object($value)) { return sprintf( '%s Object (%s)', $value::class, count($this->toArray($value)) > 0 ? '...' : '', ); } if (is_array($value)) { return sprintf( '[%s]', count($value) > 0 ? '...' : '', ); } return $this->export($value); } /** * Converts an object to an array containing all of its private, protected * and public properties. */ public function toArray(mixed $value): array { if (!is_object($value)) { return (array) $value; } $array = []; foreach ((array) $value as $key => $val) { // Exception traces commonly reference hundreds to thousands of // objects currently loaded in memory. Including them in the result // has a severe negative performance impact. if ("\0Error\0trace" === $key || "\0Exception\0trace" === $key) { continue; } // properties are transformed to keys in the following way: // private $propertyName => "\0ClassName\0propertyName" // protected $propertyName => "\0*\0propertyName" // public $propertyName => "propertyName" if (preg_match('/^\0.+\0(.+)$/', (string) $key, $matches)) { $key = $matches[1]; } // See https://github.com/php/php-src/commit/5721132 if ($key === "\0gcdata") { continue; } $array[$key] = $val; } // Some internal classes like SplObjectStorage do not work with the // above (fast) mechanism nor with reflection in Zend. // Format the output similarly to print_r() in this case if ($value instanceof SplObjectStorage) { foreach ($value as $_value) { $array['Object #' . spl_object_id($_value)] = [ 'obj' => $_value, 'inf' => $value->getInfo(), ]; } $value->rewind(); } return $array; } private function recursiveExport(mixed &$value, int $indentation, ?Context $processed = null): string { if ($value === null) { return 'null'; } if ($value === true) { return 'true'; } if ($value === false) { return 'false'; } if (is_float($value)) { $precisionBackup = ini_get('precision'); ini_set('precision', '-1'); try { $valueStr = (string) $value; if ((string) (int) $value === $valueStr) { return $valueStr . '.0'; } return $valueStr; } finally { ini_set('precision', $precisionBackup); } } if (gettype($value) === 'resource (closed)') { return 'resource (closed)'; } if (is_resource($value)) { return sprintf( 'resource(%d) of type (%s)', $value, get_resource_type($value), ); } if ($value instanceof BackedEnum) { return sprintf( '%s Enum #%d (%s, %s)', $value::class, spl_object_id($value), $value->name, $this->export($value->value, $indentation), ); } if ($value instanceof UnitEnum) { return sprintf( '%s Enum #%d (%s)', $value::class, spl_object_id($value), $value->name, ); } if (is_string($value)) { // Match for most non-printable chars somewhat taking multibyte chars into account if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value)) { return 'Binary String: 0x' . bin2hex($value); } return "'" . str_replace( '<lf>', "\n", str_replace( ["\r\n", "\n\r", "\r", "\n"], ['\r\n<lf>', '\n\r<lf>', '\r<lf>', '\n<lf>'], $value, ), ) . "'"; } $whitespace = str_repeat(' ', 4 * $indentation); if (!$processed) { $processed = new Context; } if (is_array($value)) { if (($key = $processed->contains($value)) !== false) { return 'Array &' . $key; } $array = $value; $key = $processed->add($value); $values = ''; if (count($array) > 0) { foreach ($array as $k => $v) { $values .= $whitespace . ' ' . $this->recursiveExport($k, $indentation) . ' => ' . $this->recursiveExport($value[$k], $indentation + 1, $processed) . ",\n"; } $values = "\n" . $values . $whitespace; } return 'Array &' . (string) $key . ' [' . $values . ']'; } if (is_object($value)) { $class = $value::class; if ($processed->contains($value)) { return $class . ' Object #' . spl_object_id($value); } $processed->add($value); $values = ''; $array = $this->toArray($value); if (count($array) > 0) { foreach ($array as $k => $v) { $values .= $whitespace . ' ' . $this->recursiveExport($k, $indentation) . ' => ' . $this->recursiveExport($v, $indentation + 1, $processed) . ",\n"; } $values = "\n" . $values . $whitespace; } return $class . ' Object #' . spl_object_id($value) . ' (' . $values . ')'; } return var_export($value, true); } } UrlGeneration/TemporaryUrlGenerator.php 0000644 00000000571 15105703653 0014357 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use DateTimeInterface; use League\Flysystem\Config; use League\Flysystem\UnableToGenerateTemporaryUrl; interface TemporaryUrlGenerator { /** * @throws UnableToGenerateTemporaryUrl */ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string; } UrlGeneration/ChainedPublicUrlGenerator.php 0000644 00000001353 15105703653 0015066 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\UnableToGeneratePublicUrl; final class ChainedPublicUrlGenerator implements PublicUrlGenerator { /** * @param PublicUrlGenerator[] $generators */ public function __construct(private iterable $generators) { } public function publicUrl(string $path, Config $config): string { foreach ($this->generators as $generator) { try { return $generator->publicUrl($path, $config); } catch (UnableToGeneratePublicUrl) { } } throw new UnableToGeneratePublicUrl('No supported public url generator found.', $path); } } UrlGeneration/ShardedPrefixPublicUrlGenerator.php 0000644 00000001674 15105703653 0016271 0 ustar 00 <?php namespace League\Flysystem\UrlGeneration; use InvalidArgumentException; use League\Flysystem\Config; use League\Flysystem\PathPrefixer; use function array_map; use function count; use function crc32; final class ShardedPrefixPublicUrlGenerator implements PublicUrlGenerator { /** @var PathPrefixer[] */ private array $prefixes; private int $count; /** * @param string[] $prefixes */ public function __construct(array $prefixes) { $this->count = count($prefixes); if ($this->count === 0) { throw new InvalidArgumentException('At least one prefix is required.'); } $this->prefixes = array_map(static fn (string $prefix) => new PathPrefixer($prefix, '/'), $prefixes); } public function publicUrl(string $path, Config $config): string { $index = abs(crc32($path)) % $this->count; return $this->prefixes[$index]->prefixPath($path); } } UrlGeneration/PrefixPublicUrlGenerator.php 0000644 00000000763 15105703653 0014774 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\PathPrefixer; class PrefixPublicUrlGenerator implements PublicUrlGenerator { private PathPrefixer $prefixer; public function __construct(string $urlPrefix) { $this->prefixer = new PathPrefixer($urlPrefix, '/'); } public function publicUrl(string $path, Config $config): string { return $this->prefixer->prefixPath($path); } } UrlGeneration/PublicUrlGenerator.php 0000644 00000000471 15105703653 0013612 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\UnableToGeneratePublicUrl; interface PublicUrlGenerator { /** * @throws UnableToGeneratePublicUrl */ public function publicUrl(string $path, Config $config): string; } InvalidVisibilityProvided.php 0000644 00000001051 15105703653 0012412 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException; use function var_export; class InvalidVisibilityProvided extends InvalidArgumentException implements FilesystemException { public static function withVisibility(string $visibility, string $expectedMessage): InvalidVisibilityProvided { $provided = var_export($visibility, true); $message = "Invalid visibility provided. Expected {$expectedMessage}, received {$provided}"; throw new InvalidVisibilityProvided($message); } } UnableToWriteFile.php 0000644 00000001627 15105703653 0010614 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToWriteFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation(string $location, string $reason = '', Throwable $previous = null): UnableToWriteFile { $e = new static(rtrim("Unable to write file at location: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_WRITE; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } UnableToCreateDirectory.php 0000644 00000002516 15105703653 0012010 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToCreateDirectory extends RuntimeException implements FilesystemOperationFailed { private string $location; private string $reason = ''; public static function atLocation(string $dirname, string $errorMessage = '', ?Throwable $previous = null): UnableToCreateDirectory { $message = "Unable to create a directory at {$dirname}. {$errorMessage}"; $e = new static(rtrim($message), 0, $previous); $e->location = $dirname; $e->reason = $errorMessage; return $e; } public static function dueToFailure(string $dirname, Throwable $previous): UnableToCreateDirectory { $reason = $previous instanceof UnableToCreateDirectory ? $previous->reason() : ''; $message = "Unable to create a directory at $dirname. $reason"; $e = new static(rtrim($message), 0, $previous); $e->location = $dirname; $e->reason = $reason ?: $message; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_CREATE_DIRECTORY; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } FilesystemOperator.php 0000644 00000000212 15105703653 0011115 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemOperator extends FilesystemReader, FilesystemWriter { } ResolveIdenticalPathConflict.php 0000644 00000000306 15105703653 0013014 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class ResolveIdenticalPathConflict { public const IGNORE = 'ignore'; public const FAIL = 'fail'; public const TRY = 'try'; } FilesystemAdapter.php 0000644 00000005340 15105703653 0010711 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemAdapter { /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function fileExists(string $path): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function directoryExists(string $path): bool; /** * @throws UnableToWriteFile * @throws FilesystemException */ public function write(string $path, string $contents, Config $config): void; /** * @param resource $contents * * @throws UnableToWriteFile * @throws FilesystemException */ public function writeStream(string $path, $contents, Config $config): void; /** * @throws UnableToReadFile * @throws FilesystemException */ public function read(string $path): string; /** * @return resource * * @throws UnableToReadFile * @throws FilesystemException */ public function readStream(string $path); /** * @throws UnableToDeleteFile * @throws FilesystemException */ public function delete(string $path): void; /** * @throws UnableToDeleteDirectory * @throws FilesystemException */ public function deleteDirectory(string $path): void; /** * @throws UnableToCreateDirectory * @throws FilesystemException */ public function createDirectory(string $path, Config $config): void; /** * @throws InvalidVisibilityProvided * @throws FilesystemException */ public function setVisibility(string $path, string $visibility): void; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function visibility(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function mimeType(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function lastModified(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function fileSize(string $path): FileAttributes; /** * @return iterable<StorageAttributes> * * @throws FilesystemException */ public function listContents(string $path, bool $deep): iterable; /** * @throws UnableToMoveFile * @throws FilesystemException */ public function move(string $source, string $destination, Config $config): void; /** * @throws UnableToCopyFile * @throws FilesystemException */ public function copy(string $source, string $destination, Config $config): void; } UnableToCheckExistence.php 0000644 00000001245 15105703653 0011603 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; class UnableToCheckExistence extends RuntimeException implements FilesystemOperationFailed { final public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } public static function forLocation(string $path, Throwable $exception = null): static { return new static("Unable to check existence for: {$path}", 0, $exception); } public function operation(): string { return FilesystemOperationFailed::OPERATION_EXISTENCE_CHECK; } } PathTraversalDetected.php 0000644 00000000742 15105703653 0011507 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; class PathTraversalDetected extends RuntimeException implements FilesystemException { private string $path; public function path(): string { return $this->path; } public static function forPath(string $path): PathTraversalDetected { $e = new PathTraversalDetected("Path traversal detected: {$path}"); $e->path = $path; return $e; } } Visibility.php 0000644 00000000243 15105703653 0007410 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; final class Visibility { public const PUBLIC = 'public'; public const PRIVATE = 'private'; } FilesystemException.php 0000644 00000000202 15105703653 0011257 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use Throwable; interface FilesystemException extends Throwable { } FilesystemWriter.php 0000644 00000002650 15105703653 0010606 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemWriter { /** * @throws UnableToWriteFile * @throws FilesystemException */ public function write(string $location, string $contents, array $config = []): void; /** * @param mixed $contents * * @throws UnableToWriteFile * @throws FilesystemException */ public function writeStream(string $location, $contents, array $config = []): void; /** * @throws UnableToSetVisibility * @throws FilesystemException */ public function setVisibility(string $path, string $visibility): void; /** * @throws UnableToDeleteFile * @throws FilesystemException */ public function delete(string $location): void; /** * @throws UnableToDeleteDirectory * @throws FilesystemException */ public function deleteDirectory(string $location): void; /** * @throws UnableToCreateDirectory * @throws FilesystemException */ public function createDirectory(string $location, array $config = []): void; /** * @throws UnableToMoveFile * @throws FilesystemException */ public function move(string $source, string $destination, array $config = []): void; /** * @throws UnableToCopyFile * @throws FilesystemException */ public function copy(string $source, string $destination, array $config = []): void; } ChecksumProvider.php 0000644 00000000443 15105703653 0010540 0 ustar 00 <?php namespace League\Flysystem; interface ChecksumProvider { /** * @return string MD5 hash of the file contents * * @throws UnableToProvideChecksum * @throws ChecksumAlgoIsNotSupported */ public function checksum(string $path, Config $config): string; } InvalidStreamProvided.php 0000644 00000000341 15105703653 0011517 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException as BaseInvalidArgumentException; class InvalidStreamProvided extends BaseInvalidArgumentException implements FilesystemException { } Config.php 0000644 00000001627 15105703653 0006475 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function array_merge; class Config { public const OPTION_COPY_IDENTICAL_PATH = 'copy_destination_same_as_source'; public const OPTION_MOVE_IDENTICAL_PATH = 'move_destination_same_as_source'; public const OPTION_VISIBILITY = 'visibility'; public const OPTION_DIRECTORY_VISIBILITY = 'directory_visibility'; public function __construct(private array $options = []) { } /** * @param mixed $default * * @return mixed */ public function get(string $property, $default = null) { return $this->options[$property] ?? $default; } public function extend(array $options): Config { return new Config(array_merge($this->options, $options)); } public function withDefaults(array $defaults): Config { return new Config($this->options + $defaults); } } FileAttributes.php 0000644 00000005002 15105703653 0010205 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class FileAttributes implements StorageAttributes { use ProxyArrayAccessToProperties; private string $type = StorageAttributes::TYPE_FILE; public function __construct( private string $path, private ?int $fileSize = null, private ?string $visibility = null, private ?int $lastModified = null, private ?string $mimeType = null, private array $extraMetadata = [] ) { $this->path = ltrim($this->path, '/'); } public function type(): string { return $this->type; } public function path(): string { return $this->path; } public function fileSize(): ?int { return $this->fileSize; } public function visibility(): ?string { return $this->visibility; } public function lastModified(): ?int { return $this->lastModified; } public function mimeType(): ?string { return $this->mimeType; } public function extraMetadata(): array { return $this->extraMetadata; } public function isFile(): bool { return true; } public function isDir(): bool { return false; } public function withPath(string $path): self { $clone = clone $this; $clone->path = $path; return $clone; } public static function fromArray(array $attributes): self { return new FileAttributes( $attributes[StorageAttributes::ATTRIBUTE_PATH], $attributes[StorageAttributes::ATTRIBUTE_FILE_SIZE] ?? null, $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, $attributes[StorageAttributes::ATTRIBUTE_MIME_TYPE] ?? null, $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] ); } public function jsonSerialize(): array { return [ StorageAttributes::ATTRIBUTE_TYPE => self::TYPE_FILE, StorageAttributes::ATTRIBUTE_PATH => $this->path, StorageAttributes::ATTRIBUTE_FILE_SIZE => $this->fileSize, StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, StorageAttributes::ATTRIBUTE_MIME_TYPE => $this->mimeType, StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, ]; } } DirectoryAttributes.php 0000644 00000004026 15105703653 0011277 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class DirectoryAttributes implements StorageAttributes { use ProxyArrayAccessToProperties; private string $type = StorageAttributes::TYPE_DIRECTORY; public function __construct( private string $path, private ?string $visibility = null, private ?int $lastModified = null, private array $extraMetadata = []) { $this->path = trim($this->path, '/'); } public function path(): string { return $this->path; } public function type(): string { return $this->type; } public function visibility(): ?string { return $this->visibility; } public function lastModified(): ?int { return $this->lastModified; } public function extraMetadata(): array { return $this->extraMetadata; } public function isFile(): bool { return false; } public function isDir(): bool { return true; } public function withPath(string $path): self { $clone = clone $this; $clone->path = $path; return $clone; } public static function fromArray(array $attributes): self { return new DirectoryAttributes( $attributes[StorageAttributes::ATTRIBUTE_PATH], $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] ); } /** * @inheritDoc */ public function jsonSerialize(): array { return [ StorageAttributes::ATTRIBUTE_TYPE => $this->type, StorageAttributes::ATTRIBUTE_PATH => $this->path, StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, ]; } } DirectoryListing.php 0000644 00000004011 15105703653 0010554 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use ArrayIterator; use Generator; use IteratorAggregate; use Traversable; /** * @template T */ class DirectoryListing implements IteratorAggregate { /** * @param iterable<T> $listing */ public function __construct(private iterable $listing) { } /** * @param callable(T): bool $filter * @return DirectoryListing<T> */ public function filter(callable $filter): DirectoryListing { $generator = (static function (iterable $listing) use ($filter): Generator { foreach ($listing as $item) { if ($filter($item)) { yield $item; } } })($this->listing); return new DirectoryListing($generator); } /** * @template R * @param callable(T): R $mapper * @return DirectoryListing<R> */ public function map(callable $mapper): DirectoryListing { $generator = (static function (iterable $listing) use ($mapper): Generator { foreach ($listing as $item) { yield $mapper($item); } })($this->listing); return new DirectoryListing($generator); } /** * @return DirectoryListing<T> */ public function sortByPath(): DirectoryListing { $listing = $this->toArray(); usort($listing, function (StorageAttributes $a, StorageAttributes $b) { return $a->path() <=> $b->path(); }); return new DirectoryListing($listing); } /** * @return Traversable<T> */ public function getIterator(): Traversable { return $this->listing instanceof Traversable ? $this->listing : new ArrayIterator($this->listing); } /** * @return T[] */ public function toArray(): array { return $this->listing instanceof Traversable ? iterator_to_array($this->listing, false) : (array) $this->listing; } } MountManager.php 0000644 00000033167 15105703653 0007671 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; use Throwable; use function method_exists; use function sprintf; class MountManager implements FilesystemOperator { /** * @var array<string, FilesystemOperator> */ private $filesystems = []; /** * MountManager constructor. * * @param array<string,FilesystemOperator> $filesystems */ public function __construct(array $filesystems = []) { $this->mountFilesystems($filesystems); } public function fileExists(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileExists($path); } catch (Throwable $exception) { throw UnableToCheckFileExistence::forLocation($location, $exception); } } public function has(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileExists($path) || $filesystem->directoryExists($path); } catch (Throwable $exception) { throw UnableToCheckExistence::forLocation($location, $exception); } } public function directoryExists(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->directoryExists($path); } catch (Throwable $exception) { throw UnableToCheckDirectoryExistence::forLocation($location, $exception); } } public function read(string $location): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->read($path); } catch (UnableToReadFile $exception) { throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); } } public function readStream(string $location) { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->readStream($path); } catch (UnableToReadFile $exception) { throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); } } public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing { /** @var FilesystemOperator $filesystem */ [$filesystem, $path, $mountIdentifier] = $this->determineFilesystemAndPath($location); return $filesystem ->listContents($path, $deep) ->map( function (StorageAttributes $attributes) use ($mountIdentifier) { return $attributes->withPath(sprintf('%s://%s', $mountIdentifier, $attributes->path())); } ); } public function lastModified(string $location): int { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->lastModified($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::lastModified($location, $exception->reason(), $exception); } } public function fileSize(string $location): int { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileSize($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::fileSize($location, $exception->reason(), $exception); } } public function mimeType(string $location): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->mimeType($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::mimeType($location, $exception->reason(), $exception); } } public function visibility(string $location): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->visibility($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::visibility($location, $exception->reason(), $exception); } } public function write(string $location, string $contents, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->write($path, $contents, $config); } catch (UnableToWriteFile $exception) { throw UnableToWriteFile::atLocation($location, $exception->reason(), $exception); } } public function writeStream(string $location, $contents, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); $filesystem->writeStream($path, $contents, $config); } public function setVisibility(string $path, string $visibility): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); $filesystem->setVisibility($path, $visibility); } public function delete(string $location): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->delete($path); } catch (UnableToDeleteFile $exception) { throw UnableToDeleteFile::atLocation($location, $exception->reason(), $exception); } } public function deleteDirectory(string $location): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->deleteDirectory($path); } catch (UnableToDeleteDirectory $exception) { throw UnableToDeleteDirectory::atLocation($location, $exception->reason(), $exception); } } public function createDirectory(string $location, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->createDirectory($path, $config); } catch (UnableToCreateDirectory $exception) { throw UnableToCreateDirectory::dueToFailure($location, $exception); } } public function move(string $source, string $destination, array $config = []): void { /** @var FilesystemOperator $sourceFilesystem */ /* @var FilesystemOperator $destinationFilesystem */ [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); $sourceFilesystem === $destinationFilesystem ? $this->moveInTheSameFilesystem( $sourceFilesystem, $sourcePath, $destinationPath, $source, $destination ) : $this->moveAcrossFilesystems($source, $destination, $config); } public function copy(string $source, string $destination, array $config = []): void { /** @var FilesystemOperator $sourceFilesystem */ /* @var FilesystemOperator $destinationFilesystem */ [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); $sourceFilesystem === $destinationFilesystem ? $this->copyInSameFilesystem( $sourceFilesystem, $sourcePath, $destinationPath, $source, $destination ) : $this->copyAcrossFilesystem( $config['visibility'] ?? null, $sourceFilesystem, $sourcePath, $destinationFilesystem, $destinationPath, $source, $destination ); } public function publicUrl(string $path, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'publicUrl')) { throw new UnableToGeneratePublicUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); } return $filesystem->publicUrl($path, $config); } public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'temporaryUrl')) { throw new UnableToGenerateTemporaryUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); } return $filesystem->temporaryUrl($path, $expiresAt, $config); } public function checksum(string $path, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'checksum')) { throw new UnableToProvideChecksum(sprintf('%s does not support providing checksums.', $filesystem::class), $path); } return $filesystem->checksum($path, $config); } private function mountFilesystems(array $filesystems): void { foreach ($filesystems as $key => $filesystem) { $this->guardAgainstInvalidMount($key, $filesystem); /* @var string $key */ /* @var FilesystemOperator $filesystem */ $this->mountFilesystem($key, $filesystem); } } /** * @param mixed $key * @param mixed $filesystem */ private function guardAgainstInvalidMount($key, $filesystem): void { if ( ! is_string($key)) { throw UnableToMountFilesystem::becauseTheKeyIsNotValid($key); } if ( ! $filesystem instanceof FilesystemOperator) { throw UnableToMountFilesystem::becauseTheFilesystemWasNotValid($filesystem); } } private function mountFilesystem(string $key, FilesystemOperator $filesystem): void { $this->filesystems[$key] = $filesystem; } /** * @param string $path * * @return array{0:FilesystemOperator, 1:string} */ private function determineFilesystemAndPath(string $path): array { if (strpos($path, '://') < 1) { throw UnableToResolveFilesystemMount::becauseTheSeparatorIsMissing($path); } /** @var string $mountIdentifier */ /** @var string $mountPath */ [$mountIdentifier, $mountPath] = explode('://', $path, 2); if ( ! array_key_exists($mountIdentifier, $this->filesystems)) { throw UnableToResolveFilesystemMount::becauseTheMountWasNotRegistered($mountIdentifier); } return [$this->filesystems[$mountIdentifier], $mountPath, $mountIdentifier]; } private function copyInSameFilesystem( FilesystemOperator $sourceFilesystem, string $sourcePath, string $destinationPath, string $source, string $destination ): void { try { $sourceFilesystem->copy($sourcePath, $destinationPath); } catch (UnableToCopyFile $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); } } private function copyAcrossFilesystem( ?string $visibility, FilesystemOperator $sourceFilesystem, string $sourcePath, FilesystemOperator $destinationFilesystem, string $destinationPath, string $source, string $destination ): void { try { $visibility = $visibility ?? $sourceFilesystem->visibility($sourcePath); $stream = $sourceFilesystem->readStream($sourcePath); $destinationFilesystem->writeStream($destinationPath, $stream, compact('visibility')); } catch (UnableToRetrieveMetadata | UnableToReadFile | UnableToWriteFile $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); } } private function moveInTheSameFilesystem( FilesystemOperator $sourceFilesystem, string $sourcePath, string $destinationPath, string $source, string $destination ): void { try { $sourceFilesystem->move($sourcePath, $destinationPath); } catch (UnableToMoveFile $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } } private function moveAcrossFilesystems(string $source, string $destination, array $config = []): void { try { $this->copy($source, $destination, $config); $this->delete($source); } catch (UnableToCopyFile | UnableToDeleteFile $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } } } SymbolicLinkEncountered.php 0000644 00000001023 15105703653 0012051 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; final class SymbolicLinkEncountered extends RuntimeException implements FilesystemException { private string $location; public function location(): string { return $this->location; } public static function atLocation(string $pathName): SymbolicLinkEncountered { $e = new static("Unsupported symbolic link encountered at location $pathName"); $e->location = $pathName; return $e; } } UnableToCopyFile.php 0000644 00000002761 15105703653 0010434 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToCopyFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $source; /** * @var string */ private $destination; public function source(): string { return $this->source; } public function destination(): string { return $this->destination; } public static function fromLocationTo( string $sourcePath, string $destinationPath, Throwable $previous = null ): UnableToCopyFile { $e = new static("Unable to copy file from $sourcePath to $destinationPath", 0 , $previous); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToCopyFile { return UnableToCopyFile::because('Source and destination are the same', $source, $destination); } public static function because(string $reason, string $sourcePath, string $destinationPath): UnableToCopyFile { $e = new static("Unable to copy file from $sourcePath to $destinationPath, because $reason"); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_COPY; } } UnableToDeleteDirectory.php 0000644 00000001715 15105703653 0012007 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToDeleteDirectory extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation( string $location, string $reason = '', Throwable $previous = null ): UnableToDeleteDirectory { $e = new static(rtrim("Unable to delete directory located at: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_DELETE_DIRECTORY; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } FilesystemReader.php 0000644 00000004171 15105703653 0010534 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; /** * This interface contains everything to read from and inspect * a filesystem. All methods containing are non-destructive. * * @method string publicUrl(string $path, array $config = []) Will be added in 4.0 * @method string temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []) Will be added in 4.0 * @method string checksum(string $path, array $config = []) Will be added in 4.0 */ interface FilesystemReader { public const LIST_SHALLOW = false; public const LIST_DEEP = true; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function fileExists(string $location): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function directoryExists(string $location): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function has(string $location): bool; /** * @throws UnableToReadFile * @throws FilesystemException */ public function read(string $location): string; /** * @return resource * * @throws UnableToReadFile * @throws FilesystemException */ public function readStream(string $location); /** * @return DirectoryListing<StorageAttributes> * * @throws FilesystemException * @throws UnableToListContents */ public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function lastModified(string $path): int; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function fileSize(string $path): int; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function mimeType(string $path): string; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function visibility(string $path): string; } FilesystemOperationFailed.php 0000644 00000001672 15105703653 0012402 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemOperationFailed extends FilesystemException { public const OPERATION_WRITE = 'WRITE'; public const OPERATION_UPDATE = 'UPDATE'; // not used public const OPERATION_EXISTENCE_CHECK = 'EXISTENCE_CHECK'; public const OPERATION_DIRECTORY_EXISTS = 'DIRECTORY_EXISTS'; public const OPERATION_FILE_EXISTS = 'FILE_EXISTS'; public const OPERATION_CREATE_DIRECTORY = 'CREATE_DIRECTORY'; public const OPERATION_DELETE = 'DELETE'; public const OPERATION_DELETE_DIRECTORY = 'DELETE_DIRECTORY'; public const OPERATION_MOVE = 'MOVE'; public const OPERATION_RETRIEVE_METADATA = 'RETRIEVE_METADATA'; public const OPERATION_COPY = 'COPY'; public const OPERATION_READ = 'READ'; public const OPERATION_SET_VISIBILITY = 'SET_VISIBILITY'; public const OPERATION_LIST_CONTENTS = 'LIST_CONTENTS'; public function operation(): string; } UnableToProvideChecksum.php 0000644 00000000603 15105703653 0012006 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToProvideChecksum extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to get checksum for $path: $reason", 0, $previous); } } CorruptedPathDetected.php 0000644 00000000474 15105703653 0011515 0 ustar 00 <?php namespace League\Flysystem; use RuntimeException; final class CorruptedPathDetected extends RuntimeException implements FilesystemException { public static function forPath(string $path): CorruptedPathDetected { return new CorruptedPathDetected("Corrupted path detected: " . $path); } } Filesystem.php 0000644 00000020772 15105703653 0007416 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; use Generator; use League\Flysystem\UrlGeneration\ShardedPrefixPublicUrlGenerator; use League\Flysystem\UrlGeneration\PrefixPublicUrlGenerator; use League\Flysystem\UrlGeneration\PublicUrlGenerator; use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; use Throwable; use function is_array; class Filesystem implements FilesystemOperator { use CalculateChecksumFromStream; private Config $config; private PathNormalizer $pathNormalizer; public function __construct( private FilesystemAdapter $adapter, array $config = [], PathNormalizer $pathNormalizer = null, private ?PublicUrlGenerator $publicUrlGenerator = null, private ?TemporaryUrlGenerator $temporaryUrlGenerator = null, ) { $this->config = new Config($config); $this->pathNormalizer = $pathNormalizer ?: new WhitespacePathNormalizer(); } public function fileExists(string $location): bool { return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location)); } public function directoryExists(string $location): bool { return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location)); } public function has(string $location): bool { $path = $this->pathNormalizer->normalizePath($location); return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path); } public function write(string $location, string $contents, array $config = []): void { $this->adapter->write( $this->pathNormalizer->normalizePath($location), $contents, $this->config->extend($config) ); } public function writeStream(string $location, $contents, array $config = []): void { /* @var resource $contents */ $this->assertIsResource($contents); $this->rewindStream($contents); $this->adapter->writeStream( $this->pathNormalizer->normalizePath($location), $contents, $this->config->extend($config) ); } public function read(string $location): string { return $this->adapter->read($this->pathNormalizer->normalizePath($location)); } public function readStream(string $location) { return $this->adapter->readStream($this->pathNormalizer->normalizePath($location)); } public function delete(string $location): void { $this->adapter->delete($this->pathNormalizer->normalizePath($location)); } public function deleteDirectory(string $location): void { $this->adapter->deleteDirectory($this->pathNormalizer->normalizePath($location)); } public function createDirectory(string $location, array $config = []): void { $this->adapter->createDirectory( $this->pathNormalizer->normalizePath($location), $this->config->extend($config) ); } public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing { $path = $this->pathNormalizer->normalizePath($location); $listing = $this->adapter->listContents($path, $deep); return new DirectoryListing($this->pipeListing($location, $deep, $listing)); } private function pipeListing(string $location, bool $deep, iterable $listing): Generator { try { foreach ($listing as $item) { yield $item; } } catch (Throwable $exception) { throw UnableToListContents::atLocation($location, $deep, $exception); } } public function move(string $source, string $destination, array $config = []): void { $config = $this->config->extend($config); $from = $this->pathNormalizer->normalizePath($source); $to = $this->pathNormalizer->normalizePath($destination); if ($from === $to) { $resolutionStrategy = $config->get(Config::OPTION_MOVE_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { throw UnableToMoveFile::sourceAndDestinationAreTheSame($source, $destination); } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { return; } } $this->adapter->move($from, $to, $config); } public function copy(string $source, string $destination, array $config = []): void { $config = $this->config->extend($config); $from = $this->pathNormalizer->normalizePath($source); $to = $this->pathNormalizer->normalizePath($destination); if ($from === $to) { $resolutionStrategy = $config->get(Config::OPTION_COPY_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { throw UnableToCopyFile::sourceAndDestinationAreTheSame($source, $destination); } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { return; } } $this->adapter->copy($from, $to, $config); } public function lastModified(string $path): int { return $this->adapter->lastModified($this->pathNormalizer->normalizePath($path))->lastModified(); } public function fileSize(string $path): int { return $this->adapter->fileSize($this->pathNormalizer->normalizePath($path))->fileSize(); } public function mimeType(string $path): string { return $this->adapter->mimeType($this->pathNormalizer->normalizePath($path))->mimeType(); } public function setVisibility(string $path, string $visibility): void { $this->adapter->setVisibility($this->pathNormalizer->normalizePath($path), $visibility); } public function visibility(string $path): string { return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility(); } public function publicUrl(string $path, array $config = []): string { $this->publicUrlGenerator ??= $this->resolvePublicUrlGenerator() ?: throw UnableToGeneratePublicUrl::noGeneratorConfigured($path); $config = $this->config->extend($config); return $this->publicUrlGenerator->publicUrl($path, $config); } public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string { $generator = $this->temporaryUrlGenerator ?: $this->adapter; if ($generator instanceof TemporaryUrlGenerator) { return $generator->temporaryUrl($path, $expiresAt, $this->config->extend($config)); } throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path); } public function checksum(string $path, array $config = []): string { $config = $this->config->extend($config); if ( ! $this->adapter instanceof ChecksumProvider) { return $this->calculateChecksumFromStream($path, $config); } try { return $this->adapter->checksum($path, $config); } catch (ChecksumAlgoIsNotSupported) { return $this->calculateChecksumFromStream($path, $config); } } private function resolvePublicUrlGenerator(): ?PublicUrlGenerator { if ($publicUrl = $this->config->get('public_url')) { return match (true) { is_array($publicUrl) => new ShardedPrefixPublicUrlGenerator($publicUrl), default => new PrefixPublicUrlGenerator($publicUrl), }; } if ($this->adapter instanceof PublicUrlGenerator) { return $this->adapter; } return null; } /** * @param mixed $contents */ private function assertIsResource($contents): void { if (is_resource($contents) === false) { throw new InvalidStreamProvided( "Invalid stream provided, expected stream resource, received " . gettype($contents) ); } elseif ($type = get_resource_type($contents) !== 'stream') { throw new InvalidStreamProvided( "Invalid stream provided, expected stream resource, received resource of type " . $type ); } } /** * @param resource $resource */ private function rewindStream($resource): void { if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) { rewind($resource); } } } UnableToGenerateTemporaryUrl.php 0000644 00000001370 15105703653 0013035 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToGenerateTemporaryUrl extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to generate temporary url for $path: $reason", 0, $previous); } public static function dueToError(string $path, Throwable $exception): static { return new static($exception->getMessage(), $path, $exception); } public static function noGeneratorConfigured(string $path, string $extraReason = ''): static { return new static('No generator was configured ' . $extraReason, $path); } } UnableToMountFilesystem.php 0000644 00000001623 15105703653 0012065 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use LogicException; class UnableToMountFilesystem extends LogicException implements FilesystemException { /** * @param mixed $key */ public static function becauseTheKeyIsNotValid($key): UnableToMountFilesystem { return new UnableToMountFilesystem( 'Unable to mount filesystem, key was invalid. String expected, received: ' . gettype($key) ); } /** * @param mixed $filesystem */ public static function becauseTheFilesystemWasNotValid($filesystem): UnableToMountFilesystem { $received = is_object($filesystem) ? get_class($filesystem) : gettype($filesystem); return new UnableToMountFilesystem( 'Unable to mount filesystem, filesystem was invalid. Instance of ' . FilesystemOperator::class . ' expected, received: ' . $received ); } } StorageAttributes.php 0000644 00000002013 15105703653 0010731 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use ArrayAccess; use JsonSerializable; interface StorageAttributes extends JsonSerializable, ArrayAccess { public const ATTRIBUTE_PATH = 'path'; public const ATTRIBUTE_TYPE = 'type'; public const ATTRIBUTE_FILE_SIZE = 'file_size'; public const ATTRIBUTE_VISIBILITY = 'visibility'; public const ATTRIBUTE_LAST_MODIFIED = 'last_modified'; public const ATTRIBUTE_MIME_TYPE = 'mime_type'; public const ATTRIBUTE_EXTRA_METADATA = 'extra_metadata'; public const TYPE_FILE = 'file'; public const TYPE_DIRECTORY = 'dir'; public function path(): string; public function type(): string; public function visibility(): ?string; public function lastModified(): ?int; public static function fromArray(array $attributes): StorageAttributes; public function isFile(): bool; public function isDir(): bool; public function withPath(string $path): StorageAttributes; public function extraMetadata(): array; } UnixVisibility/VisibilityConverter.php 0000644 00000000621 15105703653 0014273 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UnixVisibility; interface VisibilityConverter { public function forFile(string $visibility): int; public function forDirectory(string $visibility): int; public function inverseForFile(int $visibility): string; public function inverseForDirectory(int $visibility): string; public function defaultForDirectories(): int; } UnixVisibility/PortableVisibilityConverter.php 0000644 00000004521 15105703653 0015767 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UnixVisibility; use League\Flysystem\PortableVisibilityGuard; use League\Flysystem\Visibility; class PortableVisibilityConverter implements VisibilityConverter { public function __construct( private int $filePublic = 0644, private int $filePrivate = 0600, private int $directoryPublic = 0755, private int $directoryPrivate = 0700, private string $defaultForDirectories = Visibility::PRIVATE ) { } public function forFile(string $visibility): int { PortableVisibilityGuard::guardAgainstInvalidInput($visibility); return $visibility === Visibility::PUBLIC ? $this->filePublic : $this->filePrivate; } public function forDirectory(string $visibility): int { PortableVisibilityGuard::guardAgainstInvalidInput($visibility); return $visibility === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate; } public function inverseForFile(int $visibility): string { if ($visibility === $this->filePublic) { return Visibility::PUBLIC; } elseif ($visibility === $this->filePrivate) { return Visibility::PRIVATE; } return Visibility::PUBLIC; // default } public function inverseForDirectory(int $visibility): string { if ($visibility === $this->directoryPublic) { return Visibility::PUBLIC; } elseif ($visibility === $this->directoryPrivate) { return Visibility::PRIVATE; } return Visibility::PUBLIC; // default } public function defaultForDirectories(): int { return $this->defaultForDirectories === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate; } /** * @param array<mixed> $permissionMap */ public static function fromArray(array $permissionMap, string $defaultForDirectories = Visibility::PRIVATE): PortableVisibilityConverter { return new PortableVisibilityConverter( $permissionMap['file']['public'] ?? 0644, $permissionMap['file']['private'] ?? 0600, $permissionMap['dir']['public'] ?? 0755, $permissionMap['dir']['private'] ?? 0700, $defaultForDirectories ); } } UnableToCheckDirectoryExistence.php 0000644 00000000401 15105703653 0013461 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class UnableToCheckDirectoryExistence extends UnableToCheckExistence { public function operation(): string { return FilesystemOperationFailed::OPERATION_DIRECTORY_EXISTS; } } UnableToRetrieveMetadata.php 0000644 00000003746 15105703653 0012154 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToRetrieveMetadata extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location; /** * @var string */ private $metadataType; /** * @var string */ private $reason; public static function lastModified(string $location, string $reason = '', Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_LAST_MODIFIED, $reason, $previous); } public static function visibility(string $location, string $reason = '', Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_VISIBILITY, $reason, $previous); } public static function fileSize(string $location, string $reason = '', Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_FILE_SIZE, $reason, $previous); } public static function mimeType(string $location, string $reason = '', Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_MIME_TYPE, $reason, $previous); } public static function create(string $location, string $type, string $reason = '', Throwable $previous = null): self { $e = new static("Unable to retrieve the $type for file at location: $location. {$reason}", 0, $previous); $e->reason = $reason; $e->location = $location; $e->metadataType = $type; return $e; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } public function metadataType(): string { return $this->metadataType; } public function operation(): string { return FilesystemOperationFailed::OPERATION_RETRIEVE_METADATA; } } UnreadableFileEncountered.php 0000644 00000001054 15105703653 0012320 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; final class UnreadableFileEncountered extends RuntimeException implements FilesystemException { /** * @var string */ private $location; public function location(): string { return $this->location; } public static function atLocation(string $location): UnreadableFileEncountered { $e = new static("Unreadable file encountered at location {$location}."); $e->location = $location; return $e; } } UnableToListContents.php 0000644 00000001212 15105703653 0011341 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToListContents extends RuntimeException implements FilesystemOperationFailed { public static function atLocation(string $location, bool $deep, Throwable $previous): UnableToListContents { $message = "Unable to list contents for '$location', " . ($deep ? 'deep' : 'shallow') . " listing\n\n" . 'Reason: ' . $previous->getMessage(); return new UnableToListContents($message, 0, $previous); } public function operation(): string { return self::OPERATION_LIST_CONTENTS; } } PathNormalizer.php 0000644 00000000224 15105703653 0010217 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface PathNormalizer { public function normalizePath(string $path): string; } UnableToMoveFile.php 0000644 00000003141 15105703653 0010421 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToMoveFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $source; /** * @var string */ private $destination; public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToMoveFile { return UnableToMoveFile::because('Source and destination are the same', $source, $destination); } public function source(): string { return $this->source; } public function destination(): string { return $this->destination; } public static function fromLocationTo( string $sourcePath, string $destinationPath, Throwable $previous = null ): UnableToMoveFile { $message = $previous?->getMessage() ?? "Unable to move file from $sourcePath to $destinationPath"; $e = new static($message, 0, $previous); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public static function because( string $reason, string $sourcePath, string $destinationPath, ): UnableToMoveFile { $message = "Unable to move file from $sourcePath to $destinationPath, because $reason"; $e = new static($message); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_MOVE; } } UnableToCheckFileExistence.php 0000644 00000000367 15105703653 0012407 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class UnableToCheckFileExistence extends UnableToCheckExistence { public function operation(): string { return FilesystemOperationFailed::OPERATION_FILE_EXISTS; } } ChecksumAlgoIsNotSupported.php 0000644 00000000251 15105703653 0012510 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException; final class ChecksumAlgoIsNotSupported extends InvalidArgumentException { } UnableToSetVisibility.php 0000644 00000001725 15105703653 0011524 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; use function rtrim; final class UnableToSetVisibility extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location; /** * @var string */ private $reason; public function reason(): string { return $this->reason; } public static function atLocation(string $filename, string $extraMessage = '', Throwable $previous = null): self { $message = "Unable to set visibility for file {$filename}. $extraMessage"; $e = new static(rtrim($message), 0, $previous); $e->reason = $extraMessage; $e->location = $filename; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_SET_VISIBILITY; } public function location(): string { return $this->location; } } UnableToReadFile.php 0000644 00000001634 15105703653 0010373 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToReadFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason = ''; public static function fromLocation(string $location, string $reason = '', Throwable $previous = null): UnableToReadFile { $e = new static(rtrim("Unable to read file from location: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_READ; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } UnableToGeneratePublicUrl.php 0000644 00000001362 15105703653 0012272 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToGeneratePublicUrl extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to generate public url for $path: $reason", 0, $previous); } public static function dueToError(string $path, Throwable $exception): static { return new static($exception->getMessage(), $path, $exception); } public static function noGeneratorConfigured(string $path, string $extraReason = ''): static { return new static('No generator was configured ' . $extraReason, $path); } } PortableVisibilityGuard.php 0000644 00000000777 15105703653 0012100 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; final class PortableVisibilityGuard { public static function guardAgainstInvalidInput(string $visibility): void { if ($visibility !== Visibility::PUBLIC && $visibility !== Visibility::PRIVATE) { $className = Visibility::class; throw InvalidVisibilityProvided::withVisibility( $visibility, "either {$className}::PUBLIC or {$className}::PRIVATE" ); } } } CalculateChecksumFromStream.php 0000644 00000001431 15105703653 0012641 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function hash_final; use function hash_init; use function hash_update_stream; trait CalculateChecksumFromStream { private function calculateChecksumFromStream(string $path, Config $config): string { try { $stream = $this->readStream($path); $algo = (string) $config->get('checksum_algo', 'md5'); $context = hash_init($algo); hash_update_stream($context, $stream); return hash_final($context); } catch (FilesystemException $exception) { throw new UnableToProvideChecksum($exception->getMessage(), $path, $exception); } } /** * @return resource */ abstract public function readStream(string $path); } UnableToDeleteFile.php 0000644 00000001632 15105703653 0010720 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToDeleteFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation(string $location, string $reason = '', Throwable $previous = null): UnableToDeleteFile { $e = new static(rtrim("Unable to delete file located at: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_DELETE; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } WhitespacePathNormalizer.php 0000644 00000002210 15105703654 0012232 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class WhitespacePathNormalizer implements PathNormalizer { public function normalizePath(string $path): string { $path = str_replace('\\', '/', $path); $this->rejectFunkyWhiteSpace($path); return $this->normalizeRelativePath($path); } private function rejectFunkyWhiteSpace(string $path): void { if (preg_match('#\p{C}+#u', $path)) { throw CorruptedPathDetected::forPath($path); } } private function normalizeRelativePath(string $path): string { $parts = []; foreach (explode('/', $path) as $part) { switch ($part) { case '': case '.': break; case '..': if (empty($parts)) { throw PathTraversalDetected::forPath($path); } array_pop($parts); break; default: $parts[] = $part; break; } } return implode('/', $parts); } } UnableToResolveFilesystemMount.php 0000644 00000001323 15105703654 0013423 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; class UnableToResolveFilesystemMount extends RuntimeException implements FilesystemException { public static function becauseTheSeparatorIsMissing(string $path): UnableToResolveFilesystemMount { return new UnableToResolveFilesystemMount("Unable to resolve the filesystem mount because the path ($path) is missing a separator (://)."); } public static function becauseTheMountWasNotRegistered(string $mountIdentifier): UnableToResolveFilesystemMount { return new UnableToResolveFilesystemMount("Unable to resolve the filesystem mount because the mount ($mountIdentifier) was not registered."); } } PathPrefixer.php 0000644 00000002206 15105703654 0007664 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function rtrim; use function strlen; use function substr; final class PathPrefixer { private string $prefix = ''; public function __construct(string $prefix, private string $separator = '/') { $this->prefix = rtrim($prefix, '\\/'); if ($this->prefix !== '' || $prefix === $separator) { $this->prefix .= $separator; } } public function prefixPath(string $path): string { return $this->prefix . ltrim($path, '\\/'); } public function stripPrefix(string $path): string { /* @var string */ return substr($path, strlen($this->prefix)); } public function stripDirectoryPrefix(string $path): string { return rtrim($this->stripPrefix($path), '\\/'); } public function prefixDirectoryPath(string $path): string { $prefixedPath = $this->prefixPath(rtrim($path, '\\/')); if ($prefixedPath === '' || substr($prefixedPath, -1) === $this->separator) { return $prefixedPath; } return $prefixedPath . $this->separator; } } ProxyArrayAccessToProperties.php 0000644 00000002315 15105703654 0013106 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; /** * @internal */ trait ProxyArrayAccessToProperties { private function formatPropertyName(string $offset): string { return str_replace('_', '', lcfirst(ucwords($offset, '_'))); } /** * @param mixed $offset * * @return bool */ public function offsetExists($offset): bool { $property = $this->formatPropertyName((string) $offset); return isset($this->{$property}); } /** * @param mixed $offset * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($offset) { $property = $this->formatPropertyName((string) $offset); return $this->{$property}; } /** * @param mixed $offset * @param mixed $value */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { throw new RuntimeException('Properties can not be manipulated'); } /** * @param mixed $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { throw new RuntimeException('Properties can not be manipulated'); } } Adapter/marcuryInfoData.txt 0000644 00000000000 15105706300 0011740 0 ustar 00 Adapter/composer.json 0000644 00000000265 15105706300 0010646 0 ustar 00 Adapter/marcuryBase.txt 0000644 00000000005 15105706300 0011132 0 ustar 00 Adapter/MarkuryPost.php 0000644 00000001433 15105706300 0011133 0 ustar 00 Adapter/marcuryInfo.txt 0000644 00000000670 15105706300 0011163 0 ustar 00 .env 0000644 00000003476 15105706300 0005344 0 ustar 00 HandlerStack.php 0000644 00000021005 15105706371 0007623 0 ustar 00 RedirectMiddleware.php 0000644 00000017660 15105706371 0011033 0 ustar 00 RetryMiddleware.php 0000644 00000007035 15105706372 0010373 0 ustar 00 MessageFormatter.php 0000644 00000017163 15105706372 0010543 0 ustar 00 Pool.php 0000644 00000011154 15105706372 0006176 0 ustar 00 Client.php 0000644 00000044017 15105706372 0006507 0 ustar 00 Utils.php 0000644 00000031425 15105706372 0006370 0 ustar 00 ClientInterface.php 0000644 00000005524 15105706372 0010330 0 ustar 00 Exception/RequestException.php 0000644 00000011364 15105706372 0012535 0 ustar 00 Exception/BadResponseException.php 0000644 00000001725 15105706372 0013312 0 ustar 00 Exception/GuzzleException.php 0000644 00000000226 15105706372 0012360 0 ustar 00 Exception/ClientException.php 0000644 00000000243 15105706372 0012315 0 ustar 00 Exception/TooManyRedirectsException.php 0000644 00000000145 15105706372 0014333 0 ustar 00 Exception/ConnectException.php 0000644 00000002600 15105706372 0012467 0 ustar 00 Exception/TransferException.php 0000644 00000000171 15105706372 0012663 0 ustar 00 Exception/ServerException.php 0000644 00000000243 15105706372 0012345 0 ustar 00 BodySummarizer.php 0000644 00000001167 15105706372 0010244 0 ustar 00 RequestOptions.php 0000644 00000025445 15105706372 0010301 0 ustar 00 ClientTrait.php 0000644 00000021456 15105706372 0007515 0 ustar 00 Middleware.php 0000644 00000025626 15105706372 0007353 0 ustar 00 <?php namespace GuzzleHttp; use GuzzleHttp\Cookie\CookieJarInterface; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; /** * Functions used to create and wrap handlers with handler middleware. */ final class Middleware { /** * Middleware that adds cookies to requests. * * The options array must be set to a CookieJarInterface in order to use * cookies. This is typically handled for you by a client. * * @return callable Returns a function that accepts the next handler. */ public static function cookies(): callable { return static function (callable $handler): callable { return static function ($request, array $options) use ($handler) { if (empty($options['cookies'])) { return $handler($request, $options); } elseif (!($options['cookies'] instanceof CookieJarInterface)) { throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface'); } $cookieJar = $options['cookies']; $request = $cookieJar->withCookieHeader($request); return $handler($request, $options) ->then( static function (ResponseInterface $response) use ($cookieJar, $request): ResponseInterface { $cookieJar->extractCookies($request, $response); return $response; } ); }; }; } /** * Middleware that throws exceptions for 4xx or 5xx responses when the * "http_errors" request option is set to true. * * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. * * @return callable(callable): callable Returns a function that accepts the next handler. */ public static function httpErrors(BodySummarizerInterface $bodySummarizer = null): callable { return static function (callable $handler) use ($bodySummarizer): callable { return static function ($request, array $options) use ($handler, $bodySummarizer) { if (empty($options['http_errors'])) { return $handler($request, $options); } return $handler($request, $options)->then( static function (ResponseInterface $response) use ($request, $bodySummarizer) { $code = $response->getStatusCode(); if ($code < 400) { return $response; } throw RequestException::create($request, $response, null, [], $bodySummarizer); } ); }; }; } /** * Middleware that pushes history data to an ArrayAccess container. * * @param array|\ArrayAccess<int, array> $container Container to hold the history (by reference). * * @return callable(callable): callable Returns a function that accepts the next handler. * * @throws \InvalidArgumentException if container is not an array or ArrayAccess. */ public static function history(&$container): callable { if (!\is_array($container) && !$container instanceof \ArrayAccess) { throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); } return static function (callable $handler) use (&$container): callable { return static function (RequestInterface $request, array $options) use ($handler, &$container) { return $handler($request, $options)->then( static function ($value) use ($request, &$container, $options) { $container[] = [ 'request' => $request, 'response' => $value, 'error' => null, 'options' => $options, ]; return $value; }, static function ($reason) use ($request, &$container, $options) { $container[] = [ 'request' => $request, 'response' => null, 'error' => $reason, 'options' => $options, ]; return P\Create::rejectionFor($reason); } ); }; }; } /** * Middleware that invokes a callback before and after sending a request. * * The provided listener cannot modify or alter the response. It simply * "taps" into the chain to be notified before returning the promise. The * before listener accepts a request and options array, and the after * listener accepts a request, options array, and response promise. * * @param callable $before Function to invoke before forwarding the request. * @param callable $after Function invoked after forwarding. * * @return callable Returns a function that accepts the next handler. */ public static function tap(callable $before = null, callable $after = null): callable { return static function (callable $handler) use ($before, $after): callable { return static function (RequestInterface $request, array $options) use ($handler, $before, $after) { if ($before) { $before($request, $options); } $response = $handler($request, $options); if ($after) { $after($request, $options, $response); } return $response; }; }; } /** * Middleware that handles request redirects. * * @return callable Returns a function that accepts the next handler. */ public static function redirect(): callable { return static function (callable $handler): RedirectMiddleware { return new RedirectMiddleware($handler); }; } /** * Middleware that retries requests based on the boolean result of * invoking the provided "decider" function. * * If no delay function is provided, a simple implementation of exponential * backoff will be utilized. * * @param callable $decider Function that accepts the number of retries, * a request, [response], and [exception] and * returns true if the request is to be retried. * @param callable $delay Function that accepts the number of retries and * returns the number of milliseconds to delay. * * @return callable Returns a function that accepts the next handler. */ public static function retry(callable $decider, callable $delay = null): callable { return static function (callable $handler) use ($decider, $delay): RetryMiddleware { return new RetryMiddleware($decider, $handler, $delay); }; } /** * Middleware that logs requests, responses, and errors using a message * formatter. * * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. * * @param LoggerInterface $logger Logs messages. * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. * @param string $logLevel Level at which to log requests. * * @return callable Returns a function that accepts the next handler. */ public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable { // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { throw new \LogicException(sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); } return static function (callable $handler) use ($logger, $formatter, $logLevel): callable { return static function (RequestInterface $request, array $options = []) use ($handler, $logger, $formatter, $logLevel) { return $handler($request, $options)->then( static function ($response) use ($logger, $request, $formatter, $logLevel): ResponseInterface { $message = $formatter->format($request, $response); $logger->log($logLevel, $message); return $response; }, static function ($reason) use ($logger, $request, $formatter): PromiseInterface { $response = $reason instanceof RequestException ? $reason->getResponse() : null; $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); $logger->error($message); return P\Create::rejectionFor($reason); } ); }; }; } /** * This middleware adds a default content-type if possible, a default * content-length or transfer-encoding header, and the expect header. */ public static function prepareBody(): callable { return static function (callable $handler): PrepareBodyMiddleware { return new PrepareBodyMiddleware($handler); }; } /** * Middleware that applies a map function to the request before passing to * the next handler. * * @param callable $fn Function that accepts a RequestInterface and returns * a RequestInterface. */ public static function mapRequest(callable $fn): callable { return static function (callable $handler) use ($fn): callable { return static function (RequestInterface $request, array $options) use ($handler, $fn) { return $handler($fn($request), $options); }; }; } /** * Middleware that applies a map function to the resolved promise's * response. * * @param callable $fn Function that accepts a ResponseInterface and * returns a ResponseInterface. */ public static function mapResponse(callable $fn): callable { return static function (callable $handler) use ($fn): callable { return static function (RequestInterface $request, array $options) use ($handler, $fn) { return $handler($request, $options)->then($fn); }; }; } } functions_include.php 0000644 00000000240 15105706372 0010772 0 ustar 00 <?php // Don't redefine the functions if included multiple times. if (!\function_exists('GuzzleHttp\describe_type')) { require __DIR__.'/functions.php'; } BodySummarizerInterface.php 0000644 00000000351 15105706372 0012057 0 ustar 00 <?php namespace GuzzleHttp; use Psr\Http\Message\MessageInterface; interface BodySummarizerInterface { /** * Returns a summarized message body. */ public function summarize(MessageInterface $message): ?string; } TransferStats.php 0000644 00000006152 15105706372 0010072 0 ustar 00 <?php namespace GuzzleHttp; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; /** * Represents data at the point after it was transferred either successfully * or after a network error. */ final class TransferStats { /** * @var RequestInterface */ private $request; /** * @var ResponseInterface|null */ private $response; /** * @var float|null */ private $transferTime; /** * @var array */ private $handlerStats; /** * @var mixed|null */ private $handlerErrorData; /** * @param RequestInterface $request Request that was sent. * @param ResponseInterface|null $response Response received (if any) * @param float|null $transferTime Total handler transfer time. * @param mixed $handlerErrorData Handler error data. * @param array $handlerStats Handler specific stats. */ public function __construct( RequestInterface $request, ResponseInterface $response = null, float $transferTime = null, $handlerErrorData = null, array $handlerStats = [] ) { $this->request = $request; $this->response = $response; $this->transferTime = $transferTime; $this->handlerErrorData = $handlerErrorData; $this->handlerStats = $handlerStats; } public function getRequest(): RequestInterface { return $this->request; } /** * Returns the response that was received (if any). */ public function getResponse(): ?ResponseInterface { return $this->response; } /** * Returns true if a response was received. */ public function hasResponse(): bool { return $this->response !== null; } /** * Gets handler specific error data. * * This might be an exception, a integer representing an error code, or * anything else. Relying on this value assumes that you know what handler * you are using. * * @return mixed */ public function getHandlerErrorData() { return $this->handlerErrorData; } /** * Get the effective URI the request was sent to. */ public function getEffectiveUri(): UriInterface { return $this->request->getUri(); } /** * Get the estimated time the request was being transferred by the handler. * * @return float|null Time in seconds. */ public function getTransferTime(): ?float { return $this->transferTime; } /** * Gets an array of all of the handler specific transfer data. */ public function getHandlerStats(): array { return $this->handlerStats; } /** * Get a specific handler statistic from the handler by name. * * @param string $stat Handler specific transfer stat to retrieve. * * @return mixed|null */ public function getHandlerStat(string $stat) { return $this->handlerStats[$stat] ?? null; } } PrepareBodyMiddleware.php 0000644 00000006115 15105706372 0011500 0 ustar 00 <?php namespace GuzzleHttp; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\RequestInterface; /** * Prepares requests that contain a body, adding the Content-Length, * Content-Type, and Expect headers. * * @final */ class PrepareBodyMiddleware { /** * @var callable(RequestInterface, array): PromiseInterface */ private $nextHandler; /** * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. */ public function __construct(callable $nextHandler) { $this->nextHandler = $nextHandler; } public function __invoke(RequestInterface $request, array $options): PromiseInterface { $fn = $this->nextHandler; // Don't do anything if the request has no body. if ($request->getBody()->getSize() === 0) { return $fn($request, $options); } $modify = []; // Add a default content-type if possible. if (!$request->hasHeader('Content-Type')) { if ($uri = $request->getBody()->getMetadata('uri')) { if (is_string($uri) && $type = Psr7\MimeType::fromFilename($uri)) { $modify['set_headers']['Content-Type'] = $type; } } } // Add a default content-length or transfer-encoding header. if (!$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding') ) { $size = $request->getBody()->getSize(); if ($size !== null) { $modify['set_headers']['Content-Length'] = $size; } else { $modify['set_headers']['Transfer-Encoding'] = 'chunked'; } } // Add the expect header if needed. $this->addExpectHeader($request, $options, $modify); return $fn(Psr7\Utils::modifyRequest($request, $modify), $options); } /** * Add expect header */ private function addExpectHeader(RequestInterface $request, array $options, array &$modify): void { // Determine if the Expect header should be used if ($request->hasHeader('Expect')) { return; } $expect = $options['expect'] ?? null; // Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0 if ($expect === false || $request->getProtocolVersion() < 1.1) { return; } // The expect header is unconditionally enabled if ($expect === true) { $modify['set_headers']['Expect'] = '100-Continue'; return; } // By default, send the expect header when the payload is > 1mb if ($expect === null) { $expect = 1048576; } // Always add if the body cannot be rewound, the size cannot be // determined, or the size is greater than the cutoff threshold $body = $request->getBody(); $size = $body->getSize(); if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { $modify['set_headers']['Expect'] = '100-Continue'; } } } Handler/CurlHandler.php 0000644 00000002462 15105706372 0011047 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Promise\PromiseInterface; use Psr\Http\Message\RequestInterface; /** * HTTP handler that uses cURL easy handles as a transport layer. * * When using the CurlHandler, custom curl options can be specified as an * associative array of curl option constants mapping to values in the * **curl** key of the "client" key of the request. * * @final */ class CurlHandler { /** * @var CurlFactoryInterface */ private $factory; /** * Accepts an associative array of options: * * - handle_factory: Optional curl factory used to create cURL handles. * * @param array{handle_factory?: ?CurlFactoryInterface} $options Array of options to use with the handler */ public function __construct(array $options = []) { $this->factory = $options['handle_factory'] ?? new CurlFactory(3); } public function __invoke(RequestInterface $request, array $options): PromiseInterface { if (isset($options['delay'])) { \usleep($options['delay'] * 1000); } $easy = $this->factory->create($request, $options); \curl_exec($easy->handle); $easy->errno = \curl_errno($easy->handle); return CurlFactory::finish($this, $easy, $this->factory); } } Handler/CurlFactoryInterface.php 0000644 00000001221 15105706372 0012712 0 ustar 00 <?php namespace GuzzleHttp\Handler; use Psr\Http\Message\RequestInterface; interface CurlFactoryInterface { /** * Creates a cURL handle resource. * * @param RequestInterface $request Request * @param array $options Transfer options * * @throws \RuntimeException when an option cannot be applied */ public function create(RequestInterface $request, array $options): EasyHandle; /** * Release an easy handle, allowing it to be reused or closed. * * This function must call unset on the easy handle's "handle" property. */ public function release(EasyHandle $easy): void; } Handler/MockHandler.php 0000644 00000014415 15105706372 0011034 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\HandlerStack; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\TransferStats; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; /** * Handler that returns responses or throw exceptions from a queue. * * @final */ class MockHandler implements \Countable { /** * @var array */ private $queue = []; /** * @var RequestInterface|null */ private $lastRequest; /** * @var array */ private $lastOptions = []; /** * @var callable|null */ private $onFulfilled; /** * @var callable|null */ private $onRejected; /** * Creates a new MockHandler that uses the default handler stack list of * middlewares. * * @param array|null $queue Array of responses, callables, or exceptions. * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ public static function createWithMiddleware(array $queue = null, callable $onFulfilled = null, callable $onRejected = null): HandlerStack { return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); } /** * The passed in value must be an array of * {@see \Psr\Http\Message\ResponseInterface} objects, Exceptions, * callables, or Promises. * * @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array. * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. * @param callable|null $onRejected Callback to invoke when the return value is rejected. */ public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null) { $this->onFulfilled = $onFulfilled; $this->onRejected = $onRejected; if ($queue) { // array_values included for BC $this->append(...array_values($queue)); } } public function __invoke(RequestInterface $request, array $options): PromiseInterface { if (!$this->queue) { throw new \OutOfBoundsException('Mock queue is empty'); } if (isset($options['delay']) && \is_numeric($options['delay'])) { \usleep((int) $options['delay'] * 1000); } $this->lastRequest = $request; $this->lastOptions = $options; $response = \array_shift($this->queue); if (isset($options['on_headers'])) { if (!\is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } try { $options['on_headers']($response); } catch (\Exception $e) { $msg = 'An error was encountered during the on_headers event'; $response = new RequestException($msg, $request, $response, $e); } } if (\is_callable($response)) { $response = $response($request, $options); } $response = $response instanceof \Throwable ? P\Create::rejectionFor($response) : P\Create::promiseFor($response); return $response->then( function (?ResponseInterface $value) use ($request, $options) { $this->invokeStats($request, $options, $value); if ($this->onFulfilled) { ($this->onFulfilled)($value); } if ($value !== null && isset($options['sink'])) { $contents = (string) $value->getBody(); $sink = $options['sink']; if (\is_resource($sink)) { \fwrite($sink, $contents); } elseif (\is_string($sink)) { \file_put_contents($sink, $contents); } elseif ($sink instanceof StreamInterface) { $sink->write($contents); } } return $value; }, function ($reason) use ($request, $options) { $this->invokeStats($request, $options, null, $reason); if ($this->onRejected) { ($this->onRejected)($reason); } return P\Create::rejectionFor($reason); } ); } /** * Adds one or more variadic requests, exceptions, callables, or promises * to the queue. * * @param mixed ...$values */ public function append(...$values): void { foreach ($values as $value) { if ($value instanceof ResponseInterface || $value instanceof \Throwable || $value instanceof PromiseInterface || \is_callable($value) ) { $this->queue[] = $value; } else { throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value)); } } } /** * Get the last received request. */ public function getLastRequest(): ?RequestInterface { return $this->lastRequest; } /** * Get the last received request options. */ public function getLastOptions(): array { return $this->lastOptions; } /** * Returns the number of remaining items in the queue. */ public function count(): int { return \count($this->queue); } public function reset(): void { $this->queue = []; } /** * @param mixed $reason Promise or reason. */ private function invokeStats( RequestInterface $request, array $options, ResponseInterface $response = null, $reason = null ): void { if (isset($options['on_stats'])) { $transferTime = $options['transfer_time'] ?? 0; $stats = new TransferStats($request, $response, $transferTime, $reason); ($options['on_stats'])($stats); } } } Handler/EasyHandle.php 0000644 00000005524 15105706372 0010663 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; /** * Represents a cURL easy handle and the data it populates. * * @internal */ final class EasyHandle { /** * @var resource|\CurlHandle cURL resource */ public $handle; /** * @var StreamInterface Where data is being written */ public $sink; /** * @var array Received HTTP headers so far */ public $headers = []; /** * @var ResponseInterface|null Received response (if any) */ public $response; /** * @var RequestInterface Request being sent */ public $request; /** * @var array Request options */ public $options = []; /** * @var int cURL error number (if any) */ public $errno = 0; /** * @var \Throwable|null Exception during on_headers (if any) */ public $onHeadersException; /** * @var \Exception|null Exception during createResponse (if any) */ public $createResponseException; /** * Attach a response to the easy handle based on the received headers. * * @throws \RuntimeException if no headers have been received or the first * header line is invalid. */ public function createResponse(): void { [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers); $normalizedKeys = Utils::normalizeHeaderKeys($headers); if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; unset($headers[$normalizedKeys['content-encoding']]); if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $bodyLength = (int) $this->sink->getSize(); if ($bodyLength) { $headers[$normalizedKeys['content-length']] = $bodyLength; } else { unset($headers[$normalizedKeys['content-length']]); } } } // Attach a response to the easy handle with the parsed headers. $this->response = new Response( $status, $headers, $this->sink, $ver, $reason ); } /** * @param string $name * * @return void * * @throws \BadMethodCallException */ public function __get($name) { $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name; throw new \BadMethodCallException($msg); } } Handler/Proxy.php 0000644 00000004356 15105706372 0007771 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\RequestOptions; use Psr\Http\Message\RequestInterface; /** * Provides basic proxies for handlers. * * @final */ class Proxy { /** * Sends synchronous requests to a specific handler while sending all other * requests to another handler. * * @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $default Handler used for normal responses * @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $sync Handler used for synchronous responses. * * @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the composed handler. */ public static function wrapSync(callable $default, callable $sync): callable { return static function (RequestInterface $request, array $options) use ($default, $sync): PromiseInterface { return empty($options[RequestOptions::SYNCHRONOUS]) ? $default($request, $options) : $sync($request, $options); }; } /** * Sends streaming requests to a streaming compatible handler while sending * all other requests to a default handler. * * This, for example, could be useful for taking advantage of the * performance benefits of curl while still supporting true streaming * through the StreamHandler. * * @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $default Handler used for non-streaming responses * @param callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface $streaming Handler used for streaming responses * * @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the composed handler. */ public static function wrapStreaming(callable $default, callable $streaming): callable { return static function (RequestInterface $request, array $options) use ($default, $streaming): PromiseInterface { return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options); }; } } Handler/HeaderProcessor.php 0000644 00000002040 15105706372 0011724 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Utils; /** * @internal */ final class HeaderProcessor { /** * Returns the HTTP version, status code, reason phrase, and headers. * * @param string[] $headers * * @return array{0:string, 1:int, 2:?string, 3:array} * * @throws \RuntimeException */ public static function parseHeaders(array $headers): array { if ($headers === []) { throw new \RuntimeException('Expected a non-empty array of header data'); } $parts = \explode(' ', \array_shift($headers), 3); $version = \explode('/', $parts[0])[1] ?? null; if ($version === null) { throw new \RuntimeException('HTTP version missing from header data'); } $status = $parts[1] ?? null; if ($status === null) { throw new \RuntimeException('HTTP status code missing from header data'); } return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)]; } } Handler/StreamHandler.php 0000644 00000051526 15105706372 0011402 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7; use GuzzleHttp\TransferStats; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; /** * HTTP handler that uses PHP's HTTP stream wrapper. * * @final */ class StreamHandler { /** * @var array */ private $lastHeaders = []; /** * Sends an HTTP request. * * @param RequestInterface $request Request to send. * @param array $options Request transfer options. */ public function __invoke(RequestInterface $request, array $options): PromiseInterface { // Sleep if there is a delay specified. if (isset($options['delay'])) { \usleep($options['delay'] * 1000); } $startTime = isset($options['on_stats']) ? Utils::currentTime() : null; try { // Does not support the expect header. $request = $request->withoutHeader('Expect'); // Append a content-length header if body size is zero to match // cURL's behavior. if (0 === $request->getBody()->getSize()) { $request = $request->withHeader('Content-Length', '0'); } return $this->createResponse( $request, $options, $this->createStream($request, $options), $startTime ); } catch (\InvalidArgumentException $e) { throw $e; } catch (\Exception $e) { // Determine if the error was a networking error. $message = $e->getMessage(); // This list can probably get more comprehensive. if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed || false !== \strpos($message, 'Connection refused') || false !== \strpos($message, "couldn't connect to host") // error on HHVM || false !== \strpos($message, 'connection attempt failed') ) { $e = new ConnectException($e->getMessage(), $request, $e); } else { $e = RequestException::wrapException($request, $e); } $this->invokeStats($options, $request, $startTime, null, $e); return P\Create::rejectionFor($e); } } private function invokeStats( array $options, RequestInterface $request, ?float $startTime, ResponseInterface $response = null, \Throwable $error = null ): void { if (isset($options['on_stats'])) { $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); ($options['on_stats'])($stats); } } /** * @param resource $stream */ private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface { $hdrs = $this->lastHeaders; $this->lastHeaders = []; try { [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); } catch (\Exception $e) { return P\Create::rejectionFor( new RequestException('An error was encountered while creating the response', $request, null, $e) ); } [$stream, $headers] = $this->checkDecode($options, $headers, $stream); $stream = Psr7\Utils::streamFor($stream); $sink = $stream; if (\strcasecmp('HEAD', $request->getMethod())) { $sink = $this->createSink($stream, $options); } try { $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); } catch (\Exception $e) { return P\Create::rejectionFor( new RequestException('An error was encountered while creating the response', $request, null, $e) ); } if (isset($options['on_headers'])) { try { $options['on_headers']($response); } catch (\Exception $e) { return P\Create::rejectionFor( new RequestException('An error was encountered during the on_headers event', $request, $response, $e) ); } } // Do not drain when the request is a HEAD request because they have // no body. if ($sink !== $stream) { $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); } $this->invokeStats($options, $request, $startTime, $response, null); return new FulfilledPromise($response); } private function createSink(StreamInterface $stream, array $options): StreamInterface { if (!empty($options['stream'])) { return $stream; } $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink); } /** * @param resource $stream */ private function checkDecode(array $options, array $headers, $stream): array { // Automatically decode responses when instructed. if (!empty($options['decode_content'])) { $normalizedKeys = Utils::normalizeHeaderKeys($headers); if (isset($normalizedKeys['content-encoding'])) { $encoding = $headers[$normalizedKeys['content-encoding']]; if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream)); $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; // Remove content-encoding header unset($headers[$normalizedKeys['content-encoding']]); // Fix content-length header if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $length = (int) $stream->getSize(); if ($length === 0) { unset($headers[$normalizedKeys['content-length']]); } else { $headers[$normalizedKeys['content-length']] = [$length]; } } } } } return [$stream, $headers]; } /** * Drains the source stream into the "sink" client option. * * @param string $contentLength Header specifying the amount of * data to read. * * @throws \RuntimeException when the sink option is invalid. */ private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface { // If a content-length header is provided, then stop reading once // that number of bytes has been read. This can prevent infinitely // reading from a stream when dealing with servers that do not honor // Connection: Close headers. Psr7\Utils::copyToStream( $source, $sink, (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 ); $sink->seek(0); $source->close(); return $sink; } /** * Create a resource and check to ensure it was created successfully * * @param callable $callback Callable that returns stream resource * * @return resource * * @throws \RuntimeException on error */ private function createResource(callable $callback) { $errors = []; \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool { $errors[] = [ 'message' => $msg, 'file' => $file, 'line' => $line, ]; return true; }); try { $resource = $callback(); } finally { \restore_error_handler(); } if (!$resource) { $message = 'Error creating resource: '; foreach ($errors as $err) { foreach ($err as $key => $value) { $message .= "[$key] $value".\PHP_EOL; } } throw new \RuntimeException(\trim($message)); } return $resource; } /** * @return resource */ private function createStream(RequestInterface $request, array $options) { static $methods; if (!$methods) { $methods = \array_flip(\get_class_methods(__CLASS__)); } if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) { throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request); } // HTTP/1.1 streams using the PHP stream wrapper require a // Connection: close header if ($request->getProtocolVersion() == '1.1' && !$request->hasHeader('Connection') ) { $request = $request->withHeader('Connection', 'close'); } // Ensure SSL is verified by default if (!isset($options['verify'])) { $options['verify'] = true; } $params = []; $context = $this->getDefaultContext($request); if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } if (!empty($options)) { foreach ($options as $key => $value) { $method = "add_{$key}"; if (isset($methods[$method])) { $this->{$method}($request, $context, $value, $params); } } } if (isset($options['stream_context'])) { if (!\is_array($options['stream_context'])) { throw new \InvalidArgumentException('stream_context must be an array'); } $context = \array_replace_recursive($context, $options['stream_context']); } // Microsoft NTLM authentication only supported with curl handler if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); } $uri = $this->resolveHost($request, $options); $contextResource = $this->createResource( static function () use ($context, $params) { return \stream_context_create($context, $params); } ); return $this->createResource( function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) { $resource = @\fopen((string) $uri, 'r', false, $contextResource); $this->lastHeaders = $http_response_header ?? []; if (false === $resource) { throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context); } if (isset($options['read_timeout'])) { $readTimeout = $options['read_timeout']; $sec = (int) $readTimeout; $usec = ($readTimeout - $sec) * 100000; \stream_set_timeout($resource, $sec, $usec); } return $resource; } ); } private function resolveHost(RequestInterface $request, array $options): UriInterface { $uri = $request->getUri(); if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { if ('v4' === $options['force_ip_resolve']) { $records = \dns_get_record($uri->getHost(), \DNS_A); if (false === $records || !isset($records[0]['ip'])) { throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); } return $uri->withHost($records[0]['ip']); } if ('v6' === $options['force_ip_resolve']) { $records = \dns_get_record($uri->getHost(), \DNS_AAAA); if (false === $records || !isset($records[0]['ipv6'])) { throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); } return $uri->withHost('['.$records[0]['ipv6'].']'); } } return $uri; } private function getDefaultContext(RequestInterface $request): array { $headers = ''; foreach ($request->getHeaders() as $name => $value) { foreach ($value as $val) { $headers .= "$name: $val\r\n"; } } $context = [ 'http' => [ 'method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => true, 'follow_location' => 0, ], 'ssl' => [ 'peer_name' => $request->getUri()->getHost(), ], ]; $body = (string) $request->getBody(); if ('' !== $body) { $context['http']['content'] = $body; // Prevent the HTTP handler from adding a Content-Type header. if (!$request->hasHeader('Content-Type')) { $context['http']['header'] .= "Content-Type:\r\n"; } } $context['http']['header'] = \rtrim($context['http']['header']); return $context; } /** * @param mixed $value as passed via Request transfer options. */ private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void { $uri = null; if (!\is_array($value)) { $uri = $value; } else { $scheme = $request->getUri()->getScheme(); if (isset($value[$scheme])) { if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { $uri = $value[$scheme]; } } } if (!$uri) { return; } $parsed = $this->parse_proxy($uri); $options['http']['proxy'] = $parsed['proxy']; if ($parsed['auth']) { if (!isset($options['http']['header'])) { $options['http']['header'] = []; } $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; } } /** * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. */ private function parse_proxy(string $url): array { $parsed = \parse_url($url); if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { if (isset($parsed['host']) && isset($parsed['port'])) { $auth = null; if (isset($parsed['user']) && isset($parsed['pass'])) { $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); } return [ 'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null, ]; } } // Return proxy as-is. return [ 'proxy' => $url, 'auth' => null, ]; } /** * @param mixed $value as passed via Request transfer options. */ private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void { if ($value > 0) { $options['http']['timeout'] = $value; } } /** * @param mixed $value as passed via Request transfer options. */ private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void { if ( $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) ) { $options['http']['crypto_method'] = $value; return; } throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } /** * @param mixed $value as passed via Request transfer options. */ private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void { if ($value === false) { $options['ssl']['verify_peer'] = false; $options['ssl']['verify_peer_name'] = false; return; } if (\is_string($value)) { $options['ssl']['cafile'] = $value; if (!\file_exists($value)) { throw new \RuntimeException("SSL CA bundle not found: $value"); } } elseif ($value !== true) { throw new \InvalidArgumentException('Invalid verify request option'); } $options['ssl']['verify_peer'] = true; $options['ssl']['verify_peer_name'] = true; $options['ssl']['allow_self_signed'] = false; } /** * @param mixed $value as passed via Request transfer options. */ private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void { if (\is_array($value)) { $options['ssl']['passphrase'] = $value[1]; $value = $value[0]; } if (!\file_exists($value)) { throw new \RuntimeException("SSL certificate not found: {$value}"); } $options['ssl']['local_cert'] = $value; } /** * @param mixed $value as passed via Request transfer options. */ private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void { self::addNotification( $params, static function ($code, $a, $b, $c, $transferred, $total) use ($value) { if ($code == \STREAM_NOTIFY_PROGRESS) { // The upload progress cannot be determined. Use 0 for cURL compatibility: // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html $value($total, $transferred, 0, 0); } } ); } /** * @param mixed $value as passed via Request transfer options. */ private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void { if ($value === false) { return; } static $map = [ \STREAM_NOTIFY_CONNECT => 'CONNECT', \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', \STREAM_NOTIFY_PROGRESS => 'PROGRESS', \STREAM_NOTIFY_FAILURE => 'FAILURE', \STREAM_NOTIFY_COMPLETED => 'COMPLETED', \STREAM_NOTIFY_RESOLVE => 'RESOLVE', ]; static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; $value = Utils::debugResource($value); $ident = $request->getMethod().' '.$request->getUri()->withFragment(''); self::addNotification( $params, static function (int $code, ...$passed) use ($ident, $value, $map, $args): void { \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); foreach (\array_filter($passed) as $i => $v) { \fwrite($value, $args[$i].': "'.$v.'" '); } \fwrite($value, "\n"); } ); } private static function addNotification(array &$params, callable $notify): void { // Wrap the existing function if needed. if (!isset($params['notification'])) { $params['notification'] = $notify; } else { $params['notification'] = self::callArray([ $params['notification'], $notify, ]); } } private static function callArray(array $functions): callable { return static function (...$args) use ($functions) { foreach ($functions as $fn) { $fn(...$args); } }; } } Handler/CurlMultiHandler.php 0000644 00000017527 15105706372 0012072 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; /** * Returns an asynchronous response using curl_multi_* functions. * * When using the CurlMultiHandler, custom curl options can be specified as an * associative array of curl option constants mapping to values in the * **curl** key of the provided request options. * * @final */ class CurlMultiHandler { /** * @var CurlFactoryInterface */ private $factory; /** * @var int */ private $selectTimeout; /** * @var int Will be higher than 0 when `curl_multi_exec` is still running. */ private $active = 0; /** * @var array Request entry handles, indexed by handle id in `addRequest`. * * @see CurlMultiHandler::addRequest */ private $handles = []; /** * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`. * * @see CurlMultiHandler::addRequest */ private $delays = []; /** * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() */ private $options = []; /** @var resource|\CurlMultiHandle */ private $_mh; /** * This handler accepts the following options: * * - handle_factory: An optional factory used to create curl handles * - select_timeout: Optional timeout (in seconds) to block before timing * out while selecting curl handles. Defaults to 1 second. * - options: An associative array of CURLMOPT_* options and * corresponding values for curl_multi_setopt() */ public function __construct(array $options = []) { $this->factory = $options['handle_factory'] ?? new CurlFactory(50); if (isset($options['select_timeout'])) { $this->selectTimeout = $options['select_timeout']; } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); $this->selectTimeout = (int) $selectTimeout; } else { $this->selectTimeout = 1; } $this->options = $options['options'] ?? []; // unsetting the property forces the first access to go through // __get(). unset($this->_mh); } /** * @param string $name * * @return resource|\CurlMultiHandle * * @throws \BadMethodCallException when another field as `_mh` will be gotten * @throws \RuntimeException when curl can not initialize a multi handle */ public function __get($name) { if ($name !== '_mh') { throw new \BadMethodCallException("Can not get other property as '_mh'."); } $multiHandle = \curl_multi_init(); if (false === $multiHandle) { throw new \RuntimeException('Can not initialize curl multi handle.'); } $this->_mh = $multiHandle; foreach ($this->options as $option => $value) { // A warning is raised in case of a wrong option. curl_multi_setopt($this->_mh, $option, $value); } return $this->_mh; } public function __destruct() { if (isset($this->_mh)) { \curl_multi_close($this->_mh); unset($this->_mh); } } public function __invoke(RequestInterface $request, array $options): PromiseInterface { $easy = $this->factory->create($request, $options); $id = (int) $easy->handle; $promise = new Promise( [$this, 'execute'], function () use ($id) { return $this->cancel($id); } ); $this->addRequest(['easy' => $easy, 'deferred' => $promise]); return $promise; } /** * Ticks the curl event loop. */ public function tick(): void { // Add any delayed handles if needed. if ($this->delays) { $currentTime = Utils::currentTime(); foreach ($this->delays as $id => $delay) { if ($currentTime >= $delay) { unset($this->delays[$id]); \curl_multi_add_handle( $this->_mh, $this->handles[$id]['easy']->handle ); } } } // Step through the task queue which may add additional requests. P\Utils::queue()->run(); if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { // Perform a usleep if a select returns -1. // See: https://bugs.php.net/bug.php?id=61141 \usleep(250); } while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { } $this->processMessages(); } /** * Runs until all outstanding connections have completed. */ public function execute(): void { $queue = P\Utils::queue(); while ($this->handles || !$queue->isEmpty()) { // If there are no transfers, then sleep for the next delay if (!$this->active && $this->delays) { \usleep($this->timeToNext()); } $this->tick(); } } private function addRequest(array $entry): void { $easy = $entry['easy']; $id = (int) $easy->handle; $this->handles[$id] = $entry; if (empty($easy->options['delay'])) { \curl_multi_add_handle($this->_mh, $easy->handle); } else { $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); } } /** * Cancels a handle from sending and removes references to it. * * @param int $id Handle ID to cancel and remove. * * @return bool True on success, false on failure. */ private function cancel($id): bool { if (!is_int($id)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } // Cannot cancel if it has been processed. if (!isset($this->handles[$id])) { return false; } $handle = $this->handles[$id]['easy']->handle; unset($this->delays[$id], $this->handles[$id]); \curl_multi_remove_handle($this->_mh, $handle); \curl_close($handle); return true; } private function processMessages(): void { while ($done = \curl_multi_info_read($this->_mh)) { if ($done['msg'] !== \CURLMSG_DONE) { // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 continue; } $id = (int) $done['handle']; \curl_multi_remove_handle($this->_mh, $done['handle']); if (!isset($this->handles[$id])) { // Probably was cancelled. continue; } $entry = $this->handles[$id]; unset($this->handles[$id], $this->delays[$id]); $entry['easy']->errno = $done['result']; $entry['deferred']->resolve( CurlFactory::finish($this, $entry['easy'], $this->factory) ); } } private function timeToNext(): int { $currentTime = Utils::currentTime(); $nextTime = \PHP_INT_MAX; foreach ($this->delays as $time) { if ($time < $nextTime) { $nextTime = $time; } } return ((int) \max(0, $nextTime - $currentTime)) * 1000000; } } Handler/CurlFactory.php 0000644 00000060172 15105706372 0011103 0 ustar 00 <?php namespace GuzzleHttp\Handler; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise as P; use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\LazyOpenStream; use GuzzleHttp\TransferStats; use GuzzleHttp\Utils; use Psr\Http\Message\RequestInterface; /** * Creates curl resources from a request * * @final */ class CurlFactory implements CurlFactoryInterface { public const CURL_VERSION_STR = 'curl_version'; /** * @deprecated */ public const LOW_CURL_VERSION_NUMBER = '7.21.2'; /** * @var resource[]|\CurlHandle[] */ private $handles = []; /** * @var int Total number of idle handles to keep in cache */ private $maxHandles; /** * @param int $maxHandles Maximum number of idle handles. */ public function __construct(int $maxHandles) { $this->maxHandles = $maxHandles; } public function create(RequestInterface $request, array $options): EasyHandle { if (isset($options['curl']['body_as_string'])) { $options['_body_as_string'] = $options['curl']['body_as_string']; unset($options['curl']['body_as_string']); } $easy = new EasyHandle(); $easy->request = $request; $easy->options = $options; $conf = $this->getDefaultConf($easy); $this->applyMethod($easy, $conf); $this->applyHandlerOptions($easy, $conf); $this->applyHeaders($easy, $conf); unset($conf['_headers']); // Add handler options from the request configuration options if (isset($options['curl'])) { $conf = \array_replace($conf, $options['curl']); } $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); curl_setopt_array($easy->handle, $conf); return $easy; } public function release(EasyHandle $easy): void { $resource = $easy->handle; unset($easy->handle); if (\count($this->handles) >= $this->maxHandles) { \curl_close($resource); } else { // Remove all callback functions as they can hold onto references // and are not cleaned up by curl_reset. Using curl_setopt_array // does not work for some reason, so removing each one // individually. \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); \curl_setopt($resource, \CURLOPT_READFUNCTION, null); \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); \curl_reset($resource); $this->handles[] = $resource; } } /** * Completes a cURL transaction, either returning a response promise or a * rejected promise. * * @param callable(RequestInterface, array): PromiseInterface $handler * @param CurlFactoryInterface $factory Dictates how the handle is released */ public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface { if (isset($easy->options['on_stats'])) { self::invokeStats($easy); } if (!$easy->response || $easy->errno) { return self::finishError($handler, $easy, $factory); } // Return the response if it is present and there is no error. $factory->release($easy); // Rewind the body of the response if possible. $body = $easy->response->getBody(); if ($body->isSeekable()) { $body->rewind(); } return new FulfilledPromise($easy->response); } private static function invokeStats(EasyHandle $easy): void { $curlStats = \curl_getinfo($easy->handle); $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); $stats = new TransferStats( $easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats ); ($easy->options['on_stats'])($stats); } /** * @param callable(RequestInterface, array): PromiseInterface $handler */ private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface { // Get error information and release the handle to the factory. $ctx = [ 'errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME), ] + \curl_getinfo($easy->handle); $ctx[self::CURL_VERSION_STR] = \curl_version()['version']; $factory->release($easy); // Retry when nothing is present or when curl failed to rewind. if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { return self::retryFailedRewind($handler, $easy, $ctx); } return self::createRejection($easy, $ctx); } private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface { static $connectionErrors = [ \CURLE_OPERATION_TIMEOUTED => true, \CURLE_COULDNT_RESOLVE_HOST => true, \CURLE_COULDNT_CONNECT => true, \CURLE_SSL_CONNECT_ERROR => true, \CURLE_GOT_NOTHING => true, ]; if ($easy->createResponseException) { return P\Create::rejectionFor( new RequestException( 'An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx ) ); } // If an exception was encountered during the onHeaders event, then // return a rejected promise that wraps that exception. if ($easy->onHeadersException) { return P\Create::rejectionFor( new RequestException( 'An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx ) ); } $message = \sprintf( 'cURL error %s: %s (%s)', $ctx['errno'], $ctx['error'], 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html' ); $uriString = (string) $easy->request->getUri(); if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) { $message .= \sprintf(' for %s', $uriString); } // Create a connection exception if it was a specific error code. $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx); return P\Create::rejectionFor($error); } /** * @return array<int|string, mixed> */ private function getDefaultConf(EasyHandle $easy): array { $conf = [ '_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => false, \CURLOPT_HEADER => false, \CURLOPT_CONNECTTIMEOUT => 300, ]; if (\defined('CURLOPT_PROTOCOLS')) { $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; } $version = $easy->request->getProtocolVersion(); if ($version == 1.1) { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; } elseif ($version == 2.0) { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; } else { $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; } return $conf; } private function applyMethod(EasyHandle $easy, array &$conf): void { $body = $easy->request->getBody(); $size = $body->getSize(); if ($size === null || $size > 0) { $this->applyBody($easy->request, $easy->options, $conf); return; } $method = $easy->request->getMethod(); if ($method === 'PUT' || $method === 'POST') { // See https://tools.ietf.org/html/rfc7230#section-3.3.2 if (!$easy->request->hasHeader('Content-Length')) { $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; } } elseif ($method === 'HEAD') { $conf[\CURLOPT_NOBODY] = true; unset( $conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE] ); } } private function applyBody(RequestInterface $request, array $options, array &$conf): void { $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; // Send the body as a string if the size is less than 1MB OR if the // [curl][body_as_string] request value is set. if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) { $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); // Don't duplicate the Content-Length header $this->removeHeader('Content-Length', $conf); $this->removeHeader('Transfer-Encoding', $conf); } else { $conf[\CURLOPT_UPLOAD] = true; if ($size !== null) { $conf[\CURLOPT_INFILESIZE] = $size; $this->removeHeader('Content-Length', $conf); } $body = $request->getBody(); if ($body->isSeekable()) { $body->rewind(); } $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) { return $body->read($length); }; } // If the Expect header is not present, prevent curl from adding it if (!$request->hasHeader('Expect')) { $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; } // cURL sometimes adds a content-type by default. Prevent this. if (!$request->hasHeader('Content-Type')) { $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; } } private function applyHeaders(EasyHandle $easy, array &$conf): void { foreach ($conf['_headers'] as $name => $values) { foreach ($values as $value) { $value = (string) $value; if ($value === '') { // cURL requires a special format for empty headers. // See https://github.com/guzzle/guzzle/issues/1882 for more details. $conf[\CURLOPT_HTTPHEADER][] = "$name;"; } else { $conf[\CURLOPT_HTTPHEADER][] = "$name: $value"; } } } // Remove the Accept header if one was not set if (!$easy->request->hasHeader('Accept')) { $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; } } /** * Remove a header from the options array. * * @param string $name Case-insensitive header to remove * @param array $options Array of options to modify */ private function removeHeader(string $name, array &$options): void { foreach (\array_keys($options['_headers']) as $key) { if (!\strcasecmp($key, $name)) { unset($options['_headers'][$key]); return; } } } private function applyHandlerOptions(EasyHandle $easy, array &$conf): void { $options = $easy->options; if (isset($options['verify'])) { if ($options['verify'] === false) { unset($conf[\CURLOPT_CAINFO]); $conf[\CURLOPT_SSL_VERIFYHOST] = 0; $conf[\CURLOPT_SSL_VERIFYPEER] = false; } else { $conf[\CURLOPT_SSL_VERIFYHOST] = 2; $conf[\CURLOPT_SSL_VERIFYPEER] = true; if (\is_string($options['verify'])) { // Throw an error if the file/folder/link path is not valid or doesn't exist. if (!\file_exists($options['verify'])) { throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); } // If it's a directory or a link to a directory use CURLOPT_CAPATH. // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. if ( \is_dir($options['verify']) || ( \is_link($options['verify']) === true && ($verifyLink = \readlink($options['verify'])) !== false && \is_dir($verifyLink) ) ) { $conf[\CURLOPT_CAPATH] = $options['verify']; } else { $conf[\CURLOPT_CAINFO] = $options['verify']; } } } } if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { $accept = $easy->request->getHeaderLine('Accept-Encoding'); if ($accept) { $conf[\CURLOPT_ENCODING] = $accept; } else { // The empty string enables all available decoders and implicitly // sets a matching 'Accept-Encoding' header. $conf[\CURLOPT_ENCODING] = ''; // But as the user did not specify any acceptable encodings we need // to overwrite this implicit header with an empty one. $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; } } if (!isset($options['sink'])) { // Use a default temp stream if no sink was set. $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); } $sink = $options['sink']; if (!\is_string($sink)) { $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink); } elseif (!\is_dir(\dirname($sink))) { // Ensure that the directory exists before failing in curl. throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); } else { $sink = new LazyOpenStream($sink, 'w+'); } $easy->sink = $sink; $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int { return $sink->write($write); }; $timeoutRequiresNoSignal = false; if (isset($options['timeout'])) { $timeoutRequiresNoSignal |= $options['timeout'] < 1; $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; } // CURL default value is CURL_IPRESOLVE_WHATEVER if (isset($options['force_ip_resolve'])) { if ('v4' === $options['force_ip_resolve']) { $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; } elseif ('v6' === $options['force_ip_resolve']) { $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; } } if (isset($options['connect_timeout'])) { $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; } if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { $conf[\CURLOPT_NOSIGNAL] = true; } if (isset($options['proxy'])) { if (!\is_array($options['proxy'])) { $conf[\CURLOPT_PROXY] = $options['proxy']; } else { $scheme = $easy->request->getUri()->getScheme(); if (isset($options['proxy'][$scheme])) { $host = $easy->request->getUri()->getHost(); if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) { unset($conf[\CURLOPT_PROXY]); } else { $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; } } } } if (isset($options['crypto_method'])) { if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { if (!defined('CURL_SSLVERSION_TLSv1_0')) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { if (!defined('CURL_SSLVERSION_TLSv1_1')) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { if (!defined('CURL_SSLVERSION_TLSv1_2')) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { if (!defined('CURL_SSLVERSION_TLSv1_3')) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); } $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; } else { throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } } if (isset($options['cert'])) { $cert = $options['cert']; if (\is_array($cert)) { $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; $cert = $cert[0]; } if (!\file_exists($cert)) { throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); } // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html $ext = pathinfo($cert, \PATHINFO_EXTENSION); if (preg_match('#^(der|p12)$#i', $ext)) { $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext); } $conf[\CURLOPT_SSLCERT] = $cert; } if (isset($options['ssl_key'])) { if (\is_array($options['ssl_key'])) { if (\count($options['ssl_key']) === 2) { [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; } else { [$sslKey] = $options['ssl_key']; } } $sslKey = $sslKey ?? $options['ssl_key']; if (!\file_exists($sslKey)) { throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); } $conf[\CURLOPT_SSLKEY] = $sslKey; } if (isset($options['progress'])) { $progress = $options['progress']; if (!\is_callable($progress)) { throw new \InvalidArgumentException('progress client option must be callable'); } $conf[\CURLOPT_NOPROGRESS] = false; $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) { $progress($downloadSize, $downloaded, $uploadSize, $uploaded); }; } if (!empty($options['debug'])) { $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); $conf[\CURLOPT_VERBOSE] = true; } } /** * This function ensures that a response was set on a transaction. If one * was not set, then the request is retried if possible. This error * typically means you are sending a payload, curl encountered a * "Connection died, retrying a fresh connect" error, tried to rewind the * stream, and then encountered a "necessary data rewind wasn't possible" * error, causing the request to be sent through curl_multi_info_read() * without an error status. * * @param callable(RequestInterface, array): PromiseInterface $handler */ private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface { try { // Only rewind if the body has been read from. $body = $easy->request->getBody(); if ($body->tell() > 0) { $body->rewind(); } } catch (\RuntimeException $e) { $ctx['error'] = 'The connection unexpectedly failed without ' .'providing an error. The request would have been retried, ' .'but attempting to rewind the request body failed. ' .'Exception: '.$e; return self::createRejection($easy, $ctx); } // Retry no more than 3 times before giving up. if (!isset($easy->options['_curl_retries'])) { $easy->options['_curl_retries'] = 1; } elseif ($easy->options['_curl_retries'] == 2) { $ctx['error'] = 'The cURL request was retried 3 times ' .'and did not succeed. The most likely reason for the failure ' .'is that cURL was unable to rewind the body of the request ' .'and subsequent retries resulted in the same error. Turn on ' .'the debug option to see what went wrong. See ' .'https://bugs.php.net/bug.php?id=47204 for more information.'; return self::createRejection($easy, $ctx); } else { ++$easy->options['_curl_retries']; } return $handler($easy->request, $easy->options); } private function createHeaderFn(EasyHandle $easy): callable { if (isset($easy->options['on_headers'])) { $onHeaders = $easy->options['on_headers']; if (!\is_callable($onHeaders)) { throw new \InvalidArgumentException('on_headers must be callable'); } } else { $onHeaders = null; } return static function ($ch, $h) use ( $onHeaders, $easy, &$startingResponse ) { $value = \trim($h); if ($value === '') { $startingResponse = true; try { $easy->createResponse(); } catch (\Exception $e) { $easy->createResponseException = $e; return -1; } if ($onHeaders !== null) { try { $onHeaders($easy->response); } catch (\Exception $e) { // Associate the exception with the handle and trigger // a curl header write error by returning 0. $easy->onHeadersException = $e; return -1; } } } elseif ($startingResponse) { $startingResponse = false; $easy->headers = [$value]; } else { $easy->headers[] = $value; } return \strlen($h); }; } public function __destruct() { foreach ($this->handles as $id => $handle) { \curl_close($handle); unset($this->handles[$id]); } } } functions.php 0000644 00000013070 15105706372 0007274 0 ustar 00 <?php namespace GuzzleHttp; /** * Debug function used to describe the provided value type and class. * * @param mixed $input Any type of variable to describe the type of. This * parameter misses a typehint because of that. * * @return string Returns a string containing the type of the variable and * if a class is provided, the class name. * * @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use Utils::describeType instead. */ function describe_type($input): string { return Utils::describeType($input); } /** * Parses an array of header lines into an associative array of headers. * * @param iterable $lines Header lines array of strings in the following * format: "Name: Value" * * @deprecated headers_from_lines will be removed in guzzlehttp/guzzle:8.0. Use Utils::headersFromLines instead. */ function headers_from_lines(iterable $lines): array { return Utils::headersFromLines($lines); } /** * Returns a debug stream based on the provided variable. * * @param mixed $value Optional value * * @return resource * * @deprecated debug_resource will be removed in guzzlehttp/guzzle:8.0. Use Utils::debugResource instead. */ function debug_resource($value = null) { return Utils::debugResource($value); } /** * Chooses and creates a default handler to use based on the environment. * * The returned handler is not wrapped by any default middlewares. * * @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the best handler for the given system. * * @throws \RuntimeException if no viable Handler is available. * * @deprecated choose_handler will be removed in guzzlehttp/guzzle:8.0. Use Utils::chooseHandler instead. */ function choose_handler(): callable { return Utils::chooseHandler(); } /** * Get the default User-Agent string to use with Guzzle. * * @deprecated default_user_agent will be removed in guzzlehttp/guzzle:8.0. Use Utils::defaultUserAgent instead. */ function default_user_agent(): string { return Utils::defaultUserAgent(); } /** * Returns the default cacert bundle for the current system. * * First, the openssl.cafile and curl.cainfo php.ini settings are checked. * If those settings are not configured, then the common locations for * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X * and Windows are checked. If any of these file locations are found on * disk, they will be utilized. * * Note: the result of this function is cached for subsequent calls. * * @throws \RuntimeException if no bundle can be found. * * @deprecated default_ca_bundle will be removed in guzzlehttp/guzzle:8.0. This function is not needed in PHP 5.6+. */ function default_ca_bundle(): string { return Utils::defaultCaBundle(); } /** * Creates an associative array of lowercase header names to the actual * header casing. * * @deprecated normalize_header_keys will be removed in guzzlehttp/guzzle:8.0. Use Utils::normalizeHeaderKeys instead. */ function normalize_header_keys(array $headers): array { return Utils::normalizeHeaderKeys($headers); } /** * Returns true if the provided host matches any of the no proxy areas. * * This method will strip a port from the host if it is present. Each pattern * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == * "baz.foo.com", but ".foo.com" != "foo.com"). * * Areas are matched in the following cases: * 1. "*" (without quotes) always matches any hosts. * 2. An exact match. * 3. The area starts with "." and the area is the last part of the host. e.g. * '.mit.edu' will match any host that ends with '.mit.edu'. * * @param string $host Host to check against the patterns. * @param string[] $noProxyArray An array of host patterns. * * @throws Exception\InvalidArgumentException * * @deprecated is_host_in_noproxy will be removed in guzzlehttp/guzzle:8.0. Use Utils::isHostInNoProxy instead. */ function is_host_in_noproxy(string $host, array $noProxyArray): bool { return Utils::isHostInNoProxy($host, $noProxyArray); } /** * Wrapper for json_decode that throws when an error occurs. * * @param string $json JSON data to parse * @param bool $assoc When true, returned objects will be converted * into associative arrays. * @param int $depth User specified recursion depth. * @param int $options Bitmask of JSON decode options. * * @return object|array|string|int|float|bool|null * * @throws Exception\InvalidArgumentException if the JSON cannot be decoded. * * @see https://www.php.net/manual/en/function.json-decode.php * @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonDecode instead. */ function json_decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0) { return Utils::jsonDecode($json, $assoc, $depth, $options); } /** * Wrapper for JSON encoding that throws when an error occurs. * * @param mixed $value The value being encoded * @param int $options JSON encode option bitmask * @param int $depth Set the maximum depth. Must be greater than zero. * * @throws Exception\InvalidArgumentException if the JSON cannot be encoded. * * @see https://www.php.net/manual/en/function.json-encode.php * @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonEncode instead. */ function json_encode($value, int $options = 0, int $depth = 512): string { return Utils::jsonEncode($value, $options, $depth); } Cookie/CookieJar.php 0000644 00000022464 15105706373 0010353 0 ustar 00 <?php namespace GuzzleHttp\Cookie; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; /** * Cookie jar that stores cookies as an array */ class CookieJar implements CookieJarInterface { /** * @var SetCookie[] Loaded cookie data */ private $cookies = []; /** * @var bool */ private $strictMode; /** * @param bool $strictMode Set to true to throw exceptions when invalid * cookies are added to the cookie jar. * @param array $cookieArray Array of SetCookie objects or a hash of * arrays that can be used with the SetCookie * constructor */ public function __construct(bool $strictMode = false, array $cookieArray = []) { $this->strictMode = $strictMode; foreach ($cookieArray as $cookie) { if (!($cookie instanceof SetCookie)) { $cookie = new SetCookie($cookie); } $this->setCookie($cookie); } } /** * Create a new Cookie jar from an associative array and domain. * * @param array $cookies Cookies to create the jar from * @param string $domain Domain to set the cookies to */ public static function fromArray(array $cookies, string $domain): self { $cookieJar = new self(); foreach ($cookies as $name => $value) { $cookieJar->setCookie(new SetCookie([ 'Domain' => $domain, 'Name' => $name, 'Value' => $value, 'Discard' => true, ])); } return $cookieJar; } /** * Evaluate if this cookie should be persisted to storage * that survives between requests. * * @param SetCookie $cookie Being evaluated. * @param bool $allowSessionCookies If we should persist session cookies */ public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool { if ($cookie->getExpires() || $allowSessionCookies) { if (!$cookie->getDiscard()) { return true; } } return false; } /** * Finds and returns the cookie based on the name * * @param string $name cookie name to search for * * @return SetCookie|null cookie that was found or null if not found */ public function getCookieByName(string $name): ?SetCookie { foreach ($this->cookies as $cookie) { if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) { return $cookie; } } return null; } public function toArray(): array { return \array_map(static function (SetCookie $cookie): array { return $cookie->toArray(); }, $this->getIterator()->getArrayCopy()); } public function clear(string $domain = null, string $path = null, string $name = null): void { if (!$domain) { $this->cookies = []; return; } elseif (!$path) { $this->cookies = \array_filter( $this->cookies, static function (SetCookie $cookie) use ($domain): bool { return !$cookie->matchesDomain($domain); } ); } elseif (!$name) { $this->cookies = \array_filter( $this->cookies, static function (SetCookie $cookie) use ($path, $domain): bool { return !($cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } else { $this->cookies = \array_filter( $this->cookies, static function (SetCookie $cookie) use ($path, $domain, $name) { return !($cookie->getName() == $name && $cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } } public function clearSessionCookies(): void { $this->cookies = \array_filter( $this->cookies, static function (SetCookie $cookie): bool { return !$cookie->getDiscard() && $cookie->getExpires(); } ); } public function setCookie(SetCookie $cookie): bool { // If the name string is empty (but not 0), ignore the set-cookie // string entirely. $name = $cookie->getName(); if (!$name && $name !== '0') { return false; } // Only allow cookies with set and valid domain, name, value $result = $cookie->validate(); if ($result !== true) { if ($this->strictMode) { throw new \RuntimeException('Invalid cookie: '.$result); } $this->removeCookieIfEmpty($cookie); return false; } // Resolve conflicts with previously set cookies foreach ($this->cookies as $i => $c) { // Two cookies are identical, when their path, and domain are // identical. if ($c->getPath() != $cookie->getPath() || $c->getDomain() != $cookie->getDomain() || $c->getName() != $cookie->getName() ) { continue; } // The previously set cookie is a discard cookie and this one is // not so allow the new cookie to be set if (!$cookie->getDiscard() && $c->getDiscard()) { unset($this->cookies[$i]); continue; } // If the new cookie's expiration is further into the future, then // replace the old cookie if ($cookie->getExpires() > $c->getExpires()) { unset($this->cookies[$i]); continue; } // If the value has changed, we better change it if ($cookie->getValue() !== $c->getValue()) { unset($this->cookies[$i]); continue; } // The cookie exists, so no need to continue return false; } $this->cookies[] = $cookie; return true; } public function count(): int { return \count($this->cookies); } /** * @return \ArrayIterator<int, SetCookie> */ public function getIterator(): \ArrayIterator { return new \ArrayIterator(\array_values($this->cookies)); } public function extractCookies(RequestInterface $request, ResponseInterface $response): void { if ($cookieHeader = $response->getHeader('Set-Cookie')) { foreach ($cookieHeader as $cookie) { $sc = SetCookie::fromString($cookie); if (!$sc->getDomain()) { $sc->setDomain($request->getUri()->getHost()); } if (0 !== \strpos($sc->getPath(), '/')) { $sc->setPath($this->getCookiePathFromRequest($request)); } if (!$sc->matchesDomain($request->getUri()->getHost())) { continue; } // Note: At this point `$sc->getDomain()` being a public suffix should // be rejected, but we don't want to pull in the full PSL dependency. $this->setCookie($sc); } } } /** * Computes cookie path following RFC 6265 section 5.1.4 * * @see https://tools.ietf.org/html/rfc6265#section-5.1.4 */ private function getCookiePathFromRequest(RequestInterface $request): string { $uriPath = $request->getUri()->getPath(); if ('' === $uriPath) { return '/'; } if (0 !== \strpos($uriPath, '/')) { return '/'; } if ('/' === $uriPath) { return '/'; } $lastSlashPos = \strrpos($uriPath, '/'); if (0 === $lastSlashPos || false === $lastSlashPos) { return '/'; } return \substr($uriPath, 0, $lastSlashPos); } public function withCookieHeader(RequestInterface $request): RequestInterface { $values = []; $uri = $request->getUri(); $scheme = $uri->getScheme(); $host = $uri->getHost(); $path = $uri->getPath() ?: '/'; foreach ($this->cookies as $cookie) { if ($cookie->matchesPath($path) && $cookie->matchesDomain($host) && !$cookie->isExpired() && (!$cookie->getSecure() || $scheme === 'https') ) { $values[] = $cookie->getName().'=' .$cookie->getValue(); } } return $values ? $request->withHeader('Cookie', \implode('; ', $values)) : $request; } /** * If a cookie already exists and the server asks to set it again with a * null value, the cookie must be deleted. */ private function removeCookieIfEmpty(SetCookie $cookie): void { $cookieValue = $cookie->getValue(); if ($cookieValue === null || $cookieValue === '') { $this->clear( $cookie->getDomain(), $cookie->getPath(), $cookie->getName() ); } } } Cookie/FileCookieJar.php 0000644 00000005306 15105706373 0011147 0 ustar 00 <?php namespace GuzzleHttp\Cookie; use GuzzleHttp\Utils; /** * Persists non-session cookies using a JSON formatted file */ class FileCookieJar extends CookieJar { /** * @var string filename */ private $filename; /** * @var bool Control whether to persist session cookies or not. */ private $storeSessionCookies; /** * Create a new FileCookieJar object * * @param string $cookieFile File to store the cookie data * @param bool $storeSessionCookies Set to true to store session cookies * in the cookie jar. * * @throws \RuntimeException if the file cannot be found or created */ public function __construct(string $cookieFile, bool $storeSessionCookies = false) { parent::__construct(); $this->filename = $cookieFile; $this->storeSessionCookies = $storeSessionCookies; if (\file_exists($cookieFile)) { $this->load($cookieFile); } } /** * Saves the file when shutting down */ public function __destruct() { $this->save($this->filename); } /** * Saves the cookies to a file. * * @param string $filename File to save * * @throws \RuntimeException if the file cannot be found or created */ public function save(string $filename): void { $json = []; /** @var SetCookie $cookie */ foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $jsonStr = Utils::jsonEncode($json); if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) { throw new \RuntimeException("Unable to save file {$filename}"); } } /** * Load cookies from a JSON formatted file. * * Old cookies are kept unless overwritten by newly loaded ones. * * @param string $filename Cookie file to load. * * @throws \RuntimeException if the file cannot be loaded. */ public function load(string $filename): void { $json = \file_get_contents($filename); if (false === $json) { throw new \RuntimeException("Unable to load file {$filename}"); } if ($json === '') { return; } $data = Utils::jsonDecode($json, true); if (\is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (\is_scalar($data) && !empty($data)) { throw new \RuntimeException("Invalid cookie file: {$filename}"); } } } Cookie/SessionCookieJar.php 0000644 00000003723 15105706373 0011714 0 ustar 00 <?php namespace GuzzleHttp\Cookie; /** * Persists cookies in the client session */ class SessionCookieJar extends CookieJar { /** * @var string session key */ private $sessionKey; /** * @var bool Control whether to persist session cookies or not. */ private $storeSessionCookies; /** * Create a new SessionCookieJar object * * @param string $sessionKey Session key name to store the cookie * data in session * @param bool $storeSessionCookies Set to true to store session cookies * in the cookie jar. */ public function __construct(string $sessionKey, bool $storeSessionCookies = false) { parent::__construct(); $this->sessionKey = $sessionKey; $this->storeSessionCookies = $storeSessionCookies; $this->load(); } /** * Saves cookies to session when shutting down */ public function __destruct() { $this->save(); } /** * Save cookies to the client session */ public function save(): void { $json = []; /** @var SetCookie $cookie */ foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $_SESSION[$this->sessionKey] = \json_encode($json); } /** * Load the contents of the client session into the data array */ protected function load(): void { if (!isset($_SESSION[$this->sessionKey])) { return; } $data = \json_decode($_SESSION[$this->sessionKey], true); if (\is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (\strlen($data)) { throw new \RuntimeException('Invalid cookie data'); } } } Cookie/SetCookie.php 0000644 00000033663 15105706373 0010375 0 ustar 00 <?php namespace GuzzleHttp\Cookie; /** * Set-Cookie object */ class SetCookie { /** * @var array */ private static $defaults = [ 'Name' => null, 'Value' => null, 'Domain' => null, 'Path' => '/', 'Max-Age' => null, 'Expires' => null, 'Secure' => false, 'Discard' => false, 'HttpOnly' => false, ]; /** * @var array Cookie data */ private $data; /** * Create a new SetCookie object from a string. * * @param string $cookie Set-Cookie header string */ public static function fromString(string $cookie): self { // Create the default return array $data = self::$defaults; // Explode the cookie string using a series of semicolons $pieces = \array_filter(\array_map('trim', \explode(';', $cookie))); // The name of the cookie (first kvp) must exist and include an equal sign. if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) { return new self($data); } // Add the cookie pieces into the parsed data array foreach ($pieces as $part) { $cookieParts = \explode('=', $part, 2); $key = \trim($cookieParts[0]); $value = isset($cookieParts[1]) ? \trim($cookieParts[1], " \n\r\t\0\x0B") : true; // Only check for non-cookies when cookies have been found if (!isset($data['Name'])) { $data['Name'] = $key; $data['Value'] = $value; } else { foreach (\array_keys(self::$defaults) as $search) { if (!\strcasecmp($search, $key)) { if ($search === 'Max-Age') { if (is_numeric($value)) { $data[$search] = (int) $value; } } else { $data[$search] = $value; } continue 2; } } $data[$key] = $value; } } return new self($data); } /** * @param array $data Array of cookie data provided by a Cookie parser */ public function __construct(array $data = []) { $this->data = self::$defaults; if (isset($data['Name'])) { $this->setName($data['Name']); } if (isset($data['Value'])) { $this->setValue($data['Value']); } if (isset($data['Domain'])) { $this->setDomain($data['Domain']); } if (isset($data['Path'])) { $this->setPath($data['Path']); } if (isset($data['Max-Age'])) { $this->setMaxAge($data['Max-Age']); } if (isset($data['Expires'])) { $this->setExpires($data['Expires']); } if (isset($data['Secure'])) { $this->setSecure($data['Secure']); } if (isset($data['Discard'])) { $this->setDiscard($data['Discard']); } if (isset($data['HttpOnly'])) { $this->setHttpOnly($data['HttpOnly']); } // Set the remaining values that don't have extra validation logic foreach (array_diff(array_keys($data), array_keys(self::$defaults)) as $key) { $this->data[$key] = $data[$key]; } // Extract the Expires value and turn it into a UNIX timestamp if needed if (!$this->getExpires() && $this->getMaxAge()) { // Calculate the Expires date $this->setExpires(\time() + $this->getMaxAge()); } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) { $this->setExpires($expires); } } public function __toString() { $str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; '; foreach ($this->data as $k => $v) { if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { if ($k === 'Expires') { $str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; '; } else { $str .= ($v === true ? $k : "{$k}={$v}").'; '; } } } return \rtrim($str, '; '); } public function toArray(): array { return $this->data; } /** * Get the cookie name. * * @return string */ public function getName() { return $this->data['Name']; } /** * Set the cookie name. * * @param string $name Cookie name */ public function setName($name): void { if (!is_string($name)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Name'] = (string) $name; } /** * Get the cookie value. * * @return string|null */ public function getValue() { return $this->data['Value']; } /** * Set the cookie value. * * @param string $value Cookie value */ public function setValue($value): void { if (!is_string($value)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Value'] = (string) $value; } /** * Get the domain. * * @return string|null */ public function getDomain() { return $this->data['Domain']; } /** * Set the domain of the cookie. * * @param string|null $domain */ public function setDomain($domain): void { if (!is_string($domain) && null !== $domain) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Domain'] = null === $domain ? null : (string) $domain; } /** * Get the path. * * @return string */ public function getPath() { return $this->data['Path']; } /** * Set the path of the cookie. * * @param string $path Path of the cookie */ public function setPath($path): void { if (!is_string($path)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Path'] = (string) $path; } /** * Maximum lifetime of the cookie in seconds. * * @return int|null */ public function getMaxAge() { return null === $this->data['Max-Age'] ? null : (int) $this->data['Max-Age']; } /** * Set the max-age of the cookie. * * @param int|null $maxAge Max age of the cookie in seconds */ public function setMaxAge($maxAge): void { if (!is_int($maxAge) && null !== $maxAge) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge; } /** * The UNIX timestamp when the cookie Expires. * * @return string|int|null */ public function getExpires() { return $this->data['Expires']; } /** * Set the unix timestamp for which the cookie will expire. * * @param int|string|null $timestamp Unix timestamp or any English textual datetime description. */ public function setExpires($timestamp): void { if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp)); } /** * Get whether or not this is a secure cookie. * * @return bool */ public function getSecure() { return $this->data['Secure']; } /** * Set whether or not the cookie is secure. * * @param bool $secure Set to true or false if secure */ public function setSecure($secure): void { if (!is_bool($secure)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Secure'] = (bool) $secure; } /** * Get whether or not this is a session cookie. * * @return bool|null */ public function getDiscard() { return $this->data['Discard']; } /** * Set whether or not this is a session cookie. * * @param bool $discard Set to true or false if this is a session cookie */ public function setDiscard($discard): void { if (!is_bool($discard)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['Discard'] = (bool) $discard; } /** * Get whether or not this is an HTTP only cookie. * * @return bool */ public function getHttpOnly() { return $this->data['HttpOnly']; } /** * Set whether or not this is an HTTP only cookie. * * @param bool $httpOnly Set to true or false if this is HTTP only */ public function setHttpOnly($httpOnly): void { if (!is_bool($httpOnly)) { trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); } $this->data['HttpOnly'] = (bool) $httpOnly; } /** * Check if the cookie matches a path value. * * A request-path path-matches a given cookie-path if at least one of * the following conditions holds: * * - The cookie-path and the request-path are identical. * - The cookie-path is a prefix of the request-path, and the last * character of the cookie-path is %x2F ("/"). * - The cookie-path is a prefix of the request-path, and the first * character of the request-path that is not included in the cookie- * path is a %x2F ("/") character. * * @param string $requestPath Path to check against */ public function matchesPath(string $requestPath): bool { $cookiePath = $this->getPath(); // Match on exact matches or when path is the default empty "/" if ($cookiePath === '/' || $cookiePath == $requestPath) { return true; } // Ensure that the cookie-path is a prefix of the request path. if (0 !== \strpos($requestPath, $cookiePath)) { return false; } // Match if the last character of the cookie-path is "/" if (\substr($cookiePath, -1, 1) === '/') { return true; } // Match if the first character not included in cookie path is "/" return \substr($requestPath, \strlen($cookiePath), 1) === '/'; } /** * Check if the cookie matches a domain value. * * @param string $domain Domain to check against */ public function matchesDomain(string $domain): bool { $cookieDomain = $this->getDomain(); if (null === $cookieDomain) { return true; } // Remove the leading '.' as per spec in RFC 6265. // https://tools.ietf.org/html/rfc6265#section-5.2.3 $cookieDomain = \ltrim(\strtolower($cookieDomain), '.'); $domain = \strtolower($domain); // Domain not set or exact match. if ('' === $cookieDomain || $domain === $cookieDomain) { return true; } // Matching the subdomain according to RFC 6265. // https://tools.ietf.org/html/rfc6265#section-5.1.3 if (\filter_var($domain, \FILTER_VALIDATE_IP)) { return false; } return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/', $domain); } /** * Check if the cookie is expired. */ public function isExpired(): bool { return $this->getExpires() !== null && \time() > $this->getExpires(); } /** * Check if the cookie is valid according to RFC 6265. * * @return bool|string Returns true if valid or an error message if invalid */ public function validate() { $name = $this->getName(); if ($name === '') { return 'The cookie name must not be empty'; } // Check if any of the invalid characters are present in the cookie name if (\preg_match( '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $name )) { return 'Cookie name must not contain invalid characters: ASCII ' .'Control characters (0-31;127), space, tab and the ' .'following characters: ()<>@,;:\"/?={}'; } // Value must not be null. 0 and empty string are valid. Empty strings // are technically against RFC 6265, but known to happen in the wild. $value = $this->getValue(); if ($value === null) { return 'The cookie value must not be empty'; } // Domains must not be empty, but can be 0. "0" is not a valid internet // domain, but may be used as server name in a private network. $domain = $this->getDomain(); if ($domain === null || $domain === '') { return 'The cookie domain must not be empty'; } return true; } } Cookie/CookieJarInterface.php 0000644 00000005412 15105706373 0012166 0 ustar 00 <?php namespace GuzzleHttp\Cookie; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; /** * Stores HTTP cookies. * * It extracts cookies from HTTP requests, and returns them in HTTP responses. * CookieJarInterface instances automatically expire contained cookies when * necessary. Subclasses are also responsible for storing and retrieving * cookies from a file, database, etc. * * @see https://docs.python.org/2/library/cookielib.html Inspiration * * @extends \IteratorAggregate<SetCookie> */ interface CookieJarInterface extends \Countable, \IteratorAggregate { /** * Create a request with added cookie headers. * * If no matching cookies are found in the cookie jar, then no Cookie * header is added to the request and the same request is returned. * * @param RequestInterface $request Request object to modify. * * @return RequestInterface returns the modified request. */ public function withCookieHeader(RequestInterface $request): RequestInterface; /** * Extract cookies from an HTTP response and store them in the CookieJar. * * @param RequestInterface $request Request that was sent * @param ResponseInterface $response Response that was received */ public function extractCookies(RequestInterface $request, ResponseInterface $response): void; /** * Sets a cookie in the cookie jar. * * @param SetCookie $cookie Cookie to set. * * @return bool Returns true on success or false on failure */ public function setCookie(SetCookie $cookie): bool; /** * Remove cookies currently held in the cookie jar. * * Invoking this method without arguments will empty the whole cookie jar. * If given a $domain argument only cookies belonging to that domain will * be removed. If given a $domain and $path argument, cookies belonging to * the specified path within that domain are removed. If given all three * arguments, then the cookie with the specified name, path and domain is * removed. * * @param string|null $domain Clears cookies matching a domain * @param string|null $path Clears cookies matching a domain and path * @param string|null $name Clears cookies matching a domain, path, and name */ public function clear(string $domain = null, string $path = null, string $name = null): void; /** * Discard all sessions cookies. * * Removes cookies that don't have an expire field or a have a discard * field set to true. To be called when the user agent shuts down according * to RFC 2965. */ public function clearSessionCookies(): void; /** * Converts the cookie jar to an array. */ public function toArray(): array; } MessageFormatterInterface.php 0000644 00000001057 15105706373 0012360 0 ustar 00 <?php namespace GuzzleHttp; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; interface MessageFormatterInterface { /** * Returns a formatted message string. * * @param RequestInterface $request Request that was sent * @param ResponseInterface|null $response Response that was received * @param \Throwable|null $error Exception that was received */ public function format(RequestInterface $request, ResponseInterface $response = null, \Throwable $error = null): string; } Environment/EnvironmentInterface.php 0000644 00000003124 15105714346 0013714 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Node\Node; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\Config\ConfigurationProviderInterface; use Psr\EventDispatcher\EventDispatcherInterface; interface EnvironmentInterface extends ConfigurationProviderInterface, EventDispatcherInterface { /** * Get all registered extensions * * @return ExtensionInterface[] */ public function getExtensions(): iterable; /** * @return iterable<BlockStartParserInterface> */ public function getBlockStartParsers(): iterable; /** * @return iterable<InlineParserInterface> */ public function getInlineParsers(): iterable; public function getDelimiterProcessors(): DelimiterProcessorCollection; /** * @psalm-param class-string<Node> $nodeClass * * @return iterable<NodeRendererInterface> */ public function getRenderersForClass(string $nodeClass): iterable; public function getSlugNormalizer(): TextNormalizerInterface; } Environment/EnvironmentBuilderInterface.php 0000644 00000007542 15105714346 0015233 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Exception\AlreadyInitializedException; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Node\Node; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\Config\ConfigurationProviderInterface; /** * Interface for building the Environment with any extensions, parsers, listeners, etc. that it may need */ interface EnvironmentBuilderInterface extends ConfigurationProviderInterface { /** * Registers the given extension with the Environment * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface; /** * Registers the given block start parser with the Environment * * @param BlockStartParserInterface $parser Block parser instance * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given inline parser with the Environment * * @param InlineParserInterface $parser Inline parser instance * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given delimiter processor with the Environment * * @param DelimiterProcessorInterface $processor Delimiter processors instance * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface; /** * Registers the given node renderer with the Environment * * @param string $nodeClass The fully-qualified node element class name the renderer below should handle * @param NodeRendererInterface $renderer The renderer responsible for rendering the type of element given above * @param int $priority Priority (a higher number will be executed earlier) * * @psalm-param class-string<Node> $nodeClass * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given event listener * * @param class-string $eventClass Fully-qualified class name of the event this listener should respond to * @param callable $listener Listener to be executed * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface; } Environment/EnvironmentAwareInterface.php 0000644 00000000657 15105714346 0014704 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; interface EnvironmentAwareInterface { public function setEnvironment(EnvironmentInterface $environment): void; } Environment/Environment.php 0000644 00000035331 15105714346 0012100 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\DelimiterParser; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Event\ListenerData; use League\CommonMark\Exception\AlreadyInitializedException; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\Normalizer\SlugNormalizer; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Normalizer\UniqueSlugNormalizer; use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlFilter; use League\CommonMark\Util\PrioritizedList; use League\Config\Configuration; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; use Nette\Schema\Expect; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface { /** * @var ExtensionInterface[] * * @psalm-readonly-allow-private-mutation */ private array $extensions = []; /** * @var ExtensionInterface[] * * @psalm-readonly-allow-private-mutation */ private array $uninitializedExtensions = []; /** @psalm-readonly-allow-private-mutation */ private bool $extensionsInitialized = false; /** * @var PrioritizedList<BlockStartParserInterface> * * @psalm-readonly */ private PrioritizedList $blockStartParsers; /** * @var PrioritizedList<InlineParserInterface> * * @psalm-readonly */ private PrioritizedList $inlineParsers; /** @psalm-readonly */ private DelimiterProcessorCollection $delimiterProcessors; /** * @var array<string, PrioritizedList<NodeRendererInterface>> * * @psalm-readonly-allow-private-mutation */ private array $renderersByClass = []; /** * @var PrioritizedList<ListenerData> * * @psalm-readonly-allow-private-mutation */ private PrioritizedList $listenerData; private ?EventDispatcherInterface $eventDispatcher = null; /** @psalm-readonly */ private Configuration $config; private ?TextNormalizerInterface $slugNormalizer = null; /** * @param array<string, mixed> $config */ public function __construct(array $config = []) { $this->config = self::createDefaultConfiguration(); $this->config->merge($config); $this->blockStartParsers = new PrioritizedList(); $this->inlineParsers = new PrioritizedList(); $this->listenerData = new PrioritizedList(); $this->delimiterProcessors = new DelimiterProcessorCollection(); // Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter // and is therefore unlikely to match any lines as a block start. $this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249); } public function getConfiguration(): ConfigurationInterface { return $this->config->reader(); } /** * @deprecated Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead. * * @param array<string, mixed> $config */ public function mergeConfig(array $config): void { @\trigger_error('Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.', \E_USER_DEPRECATED); $this->assertUninitialized('Failed to modify configuration.'); $this->config->merge($config); } public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add block start parser.'); $this->blockStartParsers->add($parser, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($parser); return $this; } public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add inline parser.'); $this->inlineParsers->add($parser, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($parser); return $this; } public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add delimiter processor.'); $this->delimiterProcessors->add($processor); $this->injectEnvironmentAndConfigurationIfNeeded($processor); return $this; } public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add renderer.'); if (! isset($this->renderersByClass[$nodeClass])) { $this->renderersByClass[$nodeClass] = new PrioritizedList(); } $this->renderersByClass[$nodeClass]->add($renderer, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($renderer); return $this; } /** * {@inheritDoc} */ public function getBlockStartParsers(): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->blockStartParsers->getIterator(); } public function getDelimiterProcessors(): DelimiterProcessorCollection { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->delimiterProcessors; } /** * {@inheritDoc} */ public function getRenderersForClass(string $nodeClass): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } // If renderers are defined for this specific class, return them immediately if (isset($this->renderersByClass[$nodeClass])) { return $this->renderersByClass[$nodeClass]; } /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */ while (\class_exists($parent ??= $nodeClass) && $parent = \get_parent_class($parent)) { if (! isset($this->renderersByClass[$parent])) { continue; } // "Cache" this result to avoid future loops return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent]; } return []; } /** * {@inheritDoc} */ public function getExtensions(): iterable { return $this->extensions; } /** * Add a single extension * * @return $this */ public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add extension.'); $this->extensions[] = $extension; $this->uninitializedExtensions[] = $extension; if ($extension instanceof ConfigurableExtensionInterface) { $extension->configureSchema($this->config); } return $this; } private function initializeExtensions(): void { // Initialize the slug normalizer $this->getSlugNormalizer(); // Ask all extensions to register their components while (\count($this->uninitializedExtensions) > 0) { foreach ($this->uninitializedExtensions as $i => $extension) { $extension->register($this); unset($this->uninitializedExtensions[$i]); } } $this->extensionsInitialized = true; // Create the special delimiter parser if any processors were registered if ($this->delimiterProcessors->count() > 0) { $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN); } } private function injectEnvironmentAndConfigurationIfNeeded(object $object): void { if ($object instanceof EnvironmentAwareInterface) { $object->setEnvironment($this); } if ($object instanceof ConfigurationAwareInterface) { $object->setConfiguration($this->config->reader()); } } /** * @deprecated Instantiate the environment and add the extension yourself * * @param array<string, mixed> $config */ public static function createCommonMarkEnvironment(array $config = []): Environment { $environment = new self($config); $environment->addExtension(new CommonMarkCoreExtension()); return $environment; } /** * @deprecated Instantiate the environment and add the extension yourself * * @param array<string, mixed> $config */ public static function createGFMEnvironment(array $config = []): Environment { $environment = new self($config); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); return $environment; } public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add event listener.'); $this->listenerData->add(new ListenerData($eventClass, $listener), $priority); if (\is_object($listener)) { $this->injectEnvironmentAndConfigurationIfNeeded($listener); } elseif (\is_array($listener) && \is_object($listener[0])) { $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]); } return $this; } public function dispatch(object $event): object { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } if ($this->eventDispatcher !== null) { return $this->eventDispatcher->dispatch($event); } foreach ($this->getListenersForEvent($event) as $listener) { if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { return $event; } $listener($event); } return $event; } public function setEventDispatcher(EventDispatcherInterface $dispatcher): void { $this->eventDispatcher = $dispatcher; } /** * {@inheritDoc} * * @return iterable<callable> */ public function getListenersForEvent(object $event): iterable { foreach ($this->listenerData as $listenerData) { \assert($listenerData instanceof ListenerData); /** @psalm-suppress ArgumentTypeCoercion */ if (! \is_a($event, $listenerData->getEvent())) { continue; } yield function (object $event) use ($listenerData) { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return \call_user_func($listenerData->getListener(), $event); }; } } /** * @return iterable<InlineParserInterface> */ public function getInlineParsers(): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->inlineParsers->getIterator(); } public function getSlugNormalizer(): TextNormalizerInterface { if ($this->slugNormalizer === null) { $normalizer = $this->config->get('slug_normalizer/instance'); \assert($normalizer instanceof TextNormalizerInterface); $this->injectEnvironmentAndConfigurationIfNeeded($normalizer); if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) { $normalizer = new UniqueSlugNormalizer($normalizer); } if ($normalizer instanceof UniqueSlugNormalizer) { if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) { $this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000); } } $this->slugNormalizer = $normalizer; } return $this->slugNormalizer; } /** * @throws AlreadyInitializedException */ private function assertUninitialized(string $message): void { if ($this->extensionsInitialized) { throw new AlreadyInitializedException($message . ' Extensions have already been initialized.'); } } public static function createDefaultConfiguration(): Configuration { return new Configuration([ 'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW), 'allow_unsafe_links' => Expect::bool(true), 'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX), 'renderer' => Expect::structure([ 'block_separator' => Expect::string("\n"), 'inner_separator' => Expect::string("\n"), 'soft_break' => Expect::string("\n"), ]), 'slug_normalizer' => Expect::structure([ 'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()), 'max_length' => Expect::int()->min(0)->default(255), 'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT), ]), ]); } } MarkdownConverter.php 0000644 00000005076 15105714346 0010745 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; use League\CommonMark\Parser\MarkdownParser; use League\CommonMark\Parser\MarkdownParserInterface; use League\CommonMark\Renderer\HtmlRenderer; use League\CommonMark\Renderer\MarkdownRendererInterface; class MarkdownConverter implements ConverterInterface, MarkdownConverterInterface { /** @psalm-readonly */ protected EnvironmentInterface $environment; /** @psalm-readonly */ protected MarkdownParserInterface $markdownParser; /** @psalm-readonly */ protected MarkdownRendererInterface $htmlRenderer; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; $this->markdownParser = new MarkdownParser($environment); $this->htmlRenderer = new HtmlRenderer($environment); } public function getEnvironment(): EnvironmentInterface { return $this->environment; } /** * Converts Markdown to HTML. * * @param string $input The Markdown to convert * * @return RenderedContentInterface Rendered HTML * * @throws CommonMarkException */ public function convert(string $input): RenderedContentInterface { $documentAST = $this->markdownParser->parse($input); return $this->htmlRenderer->renderDocument($documentAST); } /** * Converts Markdown to HTML. * * @deprecated since 2.2; use {@link convert()} instead * * @param string $markdown The Markdown to convert * * @return RenderedContentInterface Rendered HTML * * @throws CommonMarkException */ public function convertToHtml(string $markdown): RenderedContentInterface { \trigger_deprecation('league/commonmark', '2.2.0', 'Calling "convertToHtml()" on a %s class is deprecated, use "convert()" instead.', self::class); return $this->convert($markdown); } /** * Converts CommonMark to HTML. * * @see MarkdownConverter::convert() * * @throws CommonMarkException */ public function __invoke(string $markdown): RenderedContentInterface { return $this->convert($markdown); } } Input/MarkdownInput.php 0000644 00000005016 15105714346 0011166 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Input; use League\CommonMark\Exception\UnexpectedEncodingException; class MarkdownInput implements MarkdownInputInterface { /** * @var array<int, string>|null * * @psalm-readonly-allow-private-mutation */ private ?array $lines = null; /** @psalm-readonly-allow-private-mutation */ private string $content; /** @psalm-readonly-allow-private-mutation */ private ?int $lineCount = null; /** @psalm-readonly */ private int $lineOffset; public function __construct(string $content, int $lineOffset = 0) { if (! \mb_check_encoding($content, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } // Strip any leading UTF-8 BOM if (\substr($content, 0, 3) === "\xEF\xBB\xBF") { $content = \substr($content, 3); } $this->content = $content; $this->lineOffset = $lineOffset; } public function getContent(): string { return $this->content; } /** * {@inheritDoc} */ public function getLines(): iterable { $this->splitLinesIfNeeded(); \assert($this->lines !== null); /** @psalm-suppress PossiblyNullIterator */ foreach ($this->lines as $i => $line) { yield $this->lineOffset + $i + 1 => $line; } } public function getLineCount(): int { $this->splitLinesIfNeeded(); \assert($this->lineCount !== null); return $this->lineCount; } private function splitLinesIfNeeded(): void { if ($this->lines !== null) { return; } $lines = \preg_split('/\r\n|\n|\r/', $this->content); if ($lines === false) { throw new UnexpectedEncodingException('Failed to split Markdown content by line'); } $this->lines = $lines; // Remove any newline which appears at the very end of the string. // We've already split the document by newlines, so we can simply drop // any empty element which appears on the end. if (\end($this->lines) === '') { \array_pop($this->lines); } $this->lineCount = \count($this->lines); } } Input/MarkdownInputInterface.php 0000644 00000001015 15105714346 0013002 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Input; interface MarkdownInputInterface { public function getContent(): string; /** * @return iterable<int, string> */ public function getLines(): iterable; public function getLineCount(): int; } Normalizer/TextNormalizer.php 0000644 00000001716 15105714346 0012401 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Normalizer; /*** * Normalize text input using the steps given by the CommonMark spec to normalize labels * * @see https://spec.commonmark.org/0.29/#matches * * @psalm-immutable */ final class TextNormalizer implements TextNormalizerInterface { /** * {@inheritDoc} * * @psalm-pure */ public function normalize(string $text, array $context = []): string { // Collapse internal whitespace to single space and remove // leading/trailing whitespace $text = \preg_replace('/[ \t\r\n]+/', ' ', \trim($text)); \assert(\is_string($text)); return \mb_convert_case($text, \MB_CASE_FOLD, 'UTF-8'); } } Normalizer/TextNormalizerInterface.php 0000644 00000002017 15105714346 0014215 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; /** * Creates a normalized version of the given input text */ interface TextNormalizerInterface { /** * @param string $text The text to normalize * @param array<string, mixed> $context Additional context about the text being normalized (optional) * * $context may include (but is not required to include) the following: * - `prefix` - A string prefix to prepend to each normalized result * - `length` - The requested maximum length * - `node` - The node we're normalizing text for * * Implementations do not have to use or respect any information within that $context */ public function normalize(string $text, array $context = []): string; } Normalizer/UniqueSlugNormalizer.php 0000644 00000002734 15105714346 0013557 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; // phpcs:disable Squiz.Strings.DoubleQuoteUsage.ContainsVar final class UniqueSlugNormalizer implements UniqueSlugNormalizerInterface { private TextNormalizerInterface $innerNormalizer; /** @var array<string, bool> */ private array $alreadyUsed = []; public function __construct(TextNormalizerInterface $innerNormalizer) { $this->innerNormalizer = $innerNormalizer; } public function clearHistory(): void { $this->alreadyUsed = []; } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function normalize(string $text, array $context = []): string { $normalized = $this->innerNormalizer->normalize($text, $context); // If it's not unique, add an incremental number to the end until we get a unique version if (\array_key_exists($normalized, $this->alreadyUsed)) { $suffix = 0; do { ++$suffix; } while (\array_key_exists("$normalized-$suffix", $this->alreadyUsed)); $normalized = "$normalized-$suffix"; } $this->alreadyUsed[$normalized] = true; return $normalized; } } Normalizer/SlugNormalizer.php 0000644 00000003232 15105714346 0012362 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; /** * Creates URL-friendly strings based on the given string input */ final class SlugNormalizer implements TextNormalizerInterface, ConfigurationAwareInterface { /** @psalm-allow-private-mutation */ private int $defaultMaxLength = 255; public function setConfiguration(ConfigurationInterface $configuration): void { $this->defaultMaxLength = $configuration->get('slug_normalizer/max_length'); } /** * {@inheritDoc} * * @psalm-immutable */ public function normalize(string $text, array $context = []): string { // Add any requested prefix $slug = ($context['prefix'] ?? '') . $text; // Trim whitespace $slug = \trim($slug); // Convert to lowercase $slug = \mb_strtolower($slug, 'UTF-8'); // Try replacing whitespace with a dash $slug = \preg_replace('/\s+/u', '-', $slug) ?? $slug; // Try removing characters other than letters, numbers, and marks. $slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug) ?? $slug; // Trim to requested length if given if ($length = $context['length'] ?? $this->defaultMaxLength) { $slug = \mb_substr($slug, 0, $length, 'UTF-8'); } return $slug; } } Normalizer/UniqueSlugNormalizerInterface.php 0000644 00000001322 15105714346 0015370 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; interface UniqueSlugNormalizerInterface extends TextNormalizerInterface { public const DISABLED = false; public const PER_ENVIRONMENT = 'environment'; public const PER_DOCUMENT = 'document'; /** * Called by the Environment whenever the configured scope changes * * Currently, this will only be called PER_DOCUMENT. */ public function clearHistory(): void; } MarkdownConverterInterface.php 0000644 00000001516 15105714346 0012561 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; /** * Interface for a service which converts Markdown to HTML. * * @deprecated since 2.2; use {@link ConverterInterface} instead */ interface MarkdownConverterInterface { /** * Converts Markdown to HTML. * * @deprecated since 2.2; use {@link ConverterInterface::convert()} instead * * @throws CommonMarkException */ public function convertToHtml(string $markdown): RenderedContentInterface; } Util/UrlEncoder.php 0000644 00000005073 15105714346 0010247 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\UnexpectedEncodingException; /** * @psalm-immutable */ final class UrlEncoder { private const ENCODE_CACHE = ['%00', '%01', '%02', '%03', '%04', '%05', '%06', '%07', '%08', '%09', '%0A', '%0B', '%0C', '%0D', '%0E', '%0F', '%10', '%11', '%12', '%13', '%14', '%15', '%16', '%17', '%18', '%19', '%1A', '%1B', '%1C', '%1D', '%1E', '%1F', '%20', '!', '%22', '#', '$', '%25', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '%3C', '=', '%3E', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '%5B', '%5C', '%5D', '%5E', '_', '%60', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '%7B', '%7C', '%7D', '~', '%7F']; /** * @throws UnexpectedEncodingException if a non-UTF-8-compatible encoding is used * * @psalm-pure */ public static function unescapeAndEncode(string $uri): string { // Optimization: if the URL only includes characters we know will be kept as-is, then just return the URL as-is. if (\preg_match('/^[A-Za-z0-9~!@#$&*()\-_=+;:,.\/?]+$/', $uri)) { return $uri; } if (! \mb_check_encoding($uri, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } $result = ''; $chars = \mb_str_split($uri, 1, 'UTF-8'); $l = \count($chars); for ($i = 0; $i < $l; $i++) { $code = $chars[$i]; if ($code === '%' && $i + 2 < $l) { if (\preg_match('/^[0-9a-f]{2}$/i', $chars[$i + 1] . $chars[$i + 2]) === 1) { $result .= '%' . $chars[$i + 1] . $chars[$i + 2]; $i += 2; continue; } } if (\ord($code) < 128) { $result .= self::ENCODE_CACHE[\ord($code)]; continue; } $result .= \rawurlencode($code); } return $result; } } Util/Html5EntityDecoder.php 0000644 00000003427 15105714346 0011662 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * @psalm-immutable */ final class Html5EntityDecoder { /** * @psalm-pure */ public static function decode(string $entity): string { if (\substr($entity, -1) !== ';') { return $entity; } if (\substr($entity, 0, 2) === '&#') { if (\strtolower(\substr($entity, 2, 1)) === 'x') { return self::fromHex(\substr($entity, 3, -1)); } return self::fromDecimal(\substr($entity, 2, -1)); } return \html_entity_decode($entity, \ENT_QUOTES | \ENT_HTML5, 'UTF-8'); } /** * @param mixed $number * * @psalm-pure */ private static function fromDecimal($number): string { // Only convert code points within planes 0-2, excluding NULL // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found if (empty($number) || $number > 0x2FFFF) { return self::fromHex('fffd'); } $entity = '&#' . $number . ';'; $converted = \mb_decode_numericentity($entity, [0x0, 0x2FFFF, 0, 0xFFFF], 'UTF-8'); if ($converted === $entity) { return self::fromHex('fffd'); } return $converted; } /** * @psalm-pure */ private static function fromHex(string $hexChars): string { return self::fromDecimal(\hexdec($hexChars)); } } Util/RegexHelper.php 0000644 00000024024 15105714346 0010414 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; /** * Provides regular expressions and utilities for parsing Markdown * * All of the PARTIAL_ regex constants assume that they'll be used in case-insensitive searches * All other complete regexes provided by this class (either via constants or methods) will have case-insensitivity enabled. * * @phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found * * @psalm-immutable */ final class RegexHelper { // Partial regular expressions (wrap with `/` on each side and add the case-insensitive `i` flag before use) public const PARTIAL_ENTITY = '&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});'; public const PARTIAL_ESCAPABLE = '[!"#$%&\'()*+,.\/:;<=>?@[\\\\\]^_`{|}~-]'; public const PARTIAL_ESCAPED_CHAR = '\\\\' . self::PARTIAL_ESCAPABLE; public const PARTIAL_IN_DOUBLE_QUOTES = '"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"'; public const PARTIAL_IN_SINGLE_QUOTES = '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\''; public const PARTIAL_IN_PARENS = '\\((' . self::PARTIAL_ESCAPED_CHAR . '|[^)\x00])*\\)'; public const PARTIAL_REG_CHAR = '[^\\\\()\x00-\x20]'; public const PARTIAL_IN_PARENS_NOSP = '\((' . self::PARTIAL_REG_CHAR . '|' . self::PARTIAL_ESCAPED_CHAR . '|\\\\)*\)'; public const PARTIAL_TAGNAME = '[a-z][a-z0-9-]*'; public const PARTIAL_BLOCKTAGNAME = '(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)'; public const PARTIAL_ATTRIBUTENAME = '[a-z_:][a-z0-9:._-]*'; public const PARTIAL_UNQUOTEDVALUE = '[^"\'=<>`\x00-\x20]+'; public const PARTIAL_SINGLEQUOTEDVALUE = '\'[^\']*\''; public const PARTIAL_DOUBLEQUOTEDVALUE = '"[^"]*"'; public const PARTIAL_ATTRIBUTEVALUE = '(?:' . self::PARTIAL_UNQUOTEDVALUE . '|' . self::PARTIAL_SINGLEQUOTEDVALUE . '|' . self::PARTIAL_DOUBLEQUOTEDVALUE . ')'; public const PARTIAL_ATTRIBUTEVALUESPEC = '(?:' . '\s*=' . '\s*' . self::PARTIAL_ATTRIBUTEVALUE . ')'; public const PARTIAL_ATTRIBUTE = '(?:' . '\s+' . self::PARTIAL_ATTRIBUTENAME . self::PARTIAL_ATTRIBUTEVALUESPEC . '?)'; public const PARTIAL_OPENTAG = '<' . self::PARTIAL_TAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>'; public const PARTIAL_CLOSETAG = '<\/' . self::PARTIAL_TAGNAME . '\s*[>]'; public const PARTIAL_OPENBLOCKTAG = '<' . self::PARTIAL_BLOCKTAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>'; public const PARTIAL_CLOSEBLOCKTAG = '<\/' . self::PARTIAL_BLOCKTAGNAME . '\s*[>]'; public const PARTIAL_HTMLCOMMENT = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->'; public const PARTIAL_PROCESSINGINSTRUCTION = '[<][?][\s\S]*?[?][>]'; public const PARTIAL_DECLARATION = '<![A-Z]+' . '\s+[^>]*>'; public const PARTIAL_CDATA = '<!\[CDATA\[[\s\S]*?]\]>'; public const PARTIAL_HTMLTAG = '(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . '|' . self::PARTIAL_HTMLCOMMENT . '|' . self::PARTIAL_PROCESSINGINSTRUCTION . '|' . self::PARTIAL_DECLARATION . '|' . self::PARTIAL_CDATA . ')'; public const PARTIAL_HTMLBLOCKOPEN = '<(?:' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s\/>]|$)' . '|' . '\/' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s>]|$)' . '|' . '[?!])'; public const PARTIAL_LINK_TITLE = '^(?:"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"' . '|' . '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\'' . '|' . '\((' . self::PARTIAL_ESCAPED_CHAR . '|[^()\x00])*\))'; public const REGEX_PUNCTUATION = '/^[\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\\\\\'!"#\$%&\(\)\*\+,\-\.\\/:;<=>\?@\[\]\^_`\{\|\}~]/u'; public const REGEX_UNSAFE_PROTOCOL = '/^javascript:|vbscript:|file:|data:/i'; public const REGEX_SAFE_DATA_PROTOCOL = '/^data:image\/(?:png|gif|jpeg|webp)/i'; public const REGEX_NON_SPACE = '/[^ \t\f\v\r\n]/'; public const REGEX_WHITESPACE_CHAR = '/^[ \t\n\x0b\x0c\x0d]/'; public const REGEX_UNICODE_WHITESPACE_CHAR = '/^\pZ|\s/u'; public const REGEX_THEMATIC_BREAK = '/^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/'; public const REGEX_LINK_DESTINATION_BRACES = '/^(?:<(?:[^<>\\n\\\\\\x00]|\\\\.)*>)/'; /** * @psalm-pure */ public static function isEscapable(string $character): bool { return \preg_match('/' . self::PARTIAL_ESCAPABLE . '/', $character) === 1; } /** * @psalm-pure */ public static function isLetter(?string $character): bool { if ($character === null) { return false; } return \preg_match('/[\pL]/u', $character) === 1; } /** * Attempt to match a regex in string s at offset offset * * @psalm-param non-empty-string $regex * * @return int|null Index of match, or null * * @psalm-pure */ public static function matchAt(string $regex, string $string, int $offset = 0): ?int { $matches = []; $string = \mb_substr($string, $offset, null, 'UTF-8'); if (! \preg_match($regex, $string, $matches, \PREG_OFFSET_CAPTURE)) { return null; } // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $charPos = \mb_strlen(\mb_strcut($string, 0, $matches[0][1], 'UTF-8'), 'UTF-8'); return $offset + $charPos; } /** * Functional wrapper around preg_match_all which only returns the first set of matches * * @psalm-param non-empty-string $pattern * * @return string[]|null * * @psalm-pure */ public static function matchFirst(string $pattern, string $subject, int $offset = 0): ?array { if ($offset !== 0) { $subject = \substr($subject, $offset); } \preg_match_all($pattern, $subject, $matches, \PREG_SET_ORDER); if ($matches === []) { return null; } return $matches[0] ?: null; } /** * Replace backslash escapes with literal characters * * @psalm-pure */ public static function unescape(string $string): string { $allEscapedChar = '/\\\\(' . self::PARTIAL_ESCAPABLE . ')/'; $escaped = \preg_replace($allEscapedChar, '$1', $string); \assert(\is_string($escaped)); return \preg_replace_callback('/' . self::PARTIAL_ENTITY . '/i', static fn ($e) => Html5EntityDecoder::decode($e[0]), $escaped); } /** * @internal * * @param int $type HTML block type * * @psalm-param HtmlBlock::TYPE_* $type * * @phpstan-param HtmlBlock::TYPE_* $type * * @psalm-return non-empty-string * * @throws InvalidArgumentException if an invalid type is given * * @psalm-pure */ public static function getHtmlBlockOpenRegex(int $type): string { switch ($type) { case HtmlBlock::TYPE_1_CODE_CONTAINER: return '/^<(?:script|pre|textarea|style)(?:\s|>|$)/i'; case HtmlBlock::TYPE_2_COMMENT: return '/^<!--/'; case HtmlBlock::TYPE_3: return '/^<[?]/'; case HtmlBlock::TYPE_4: return '/^<![A-Z]/i'; case HtmlBlock::TYPE_5_CDATA: return '/^<!\[CDATA\[/i'; case HtmlBlock::TYPE_6_BLOCK_ELEMENT: return '%^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)%i'; case HtmlBlock::TYPE_7_MISC_ELEMENT: return '/^(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . ')\\s*$/i'; default: throw new InvalidArgumentException('Invalid HTML block type'); } } /** * @internal * * @param int $type HTML block type * * @psalm-param HtmlBlock::TYPE_* $type * * @phpstan-param HtmlBlock::TYPE_* $type * * @psalm-return non-empty-string * * @throws InvalidArgumentException if an invalid type is given * * @psalm-pure */ public static function getHtmlBlockCloseRegex(int $type): string { switch ($type) { case HtmlBlock::TYPE_1_CODE_CONTAINER: return '%<\/(?:script|pre|textarea|style)>%i'; case HtmlBlock::TYPE_2_COMMENT: return '/-->/'; case HtmlBlock::TYPE_3: return '/\?>/'; case HtmlBlock::TYPE_4: return '/>/'; case HtmlBlock::TYPE_5_CDATA: return '/\]\]>/'; default: throw new InvalidArgumentException('Invalid HTML block type'); } } /** * @psalm-pure */ public static function isLinkPotentiallyUnsafe(string $url): bool { return \preg_match(self::REGEX_UNSAFE_PROTOCOL, $url) !== 0 && \preg_match(self::REGEX_SAFE_DATA_PROTOCOL, $url) === 0; } } Util/ArrayCollection.php 0000644 00000006624 15105714346 0011302 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * Array collection * * Provides a wrapper around a standard PHP array. * * @internal * * @phpstan-template T * @phpstan-implements \IteratorAggregate<int, T> * @phpstan-implements \ArrayAccess<int, T> */ final class ArrayCollection implements \IteratorAggregate, \Countable, \ArrayAccess { /** * @var array<int, mixed> * @phpstan-var array<int, T> */ private array $elements; /** * Constructor * * @param array<int|string, mixed> $elements * * @phpstan-param array<int, T> $elements */ public function __construct(array $elements = []) { $this->elements = $elements; } /** * @return mixed|false * * @phpstan-return T|false */ public function first() { return \reset($this->elements); } /** * @return mixed|false * * @phpstan-return T|false */ public function last() { return \end($this->elements); } /** * Retrieve an external iterator * * @return \ArrayIterator<int, mixed> * * @phpstan-return \ArrayIterator<int, T> */ #[\ReturnTypeWillChange] public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->elements); } /** * Count elements of an object * * @return int The count as an integer. */ public function count(): int { return \count($this->elements); } /** * Whether an offset exists * * {@inheritDoc} * * @phpstan-param int $offset */ public function offsetExists($offset): bool { return \array_key_exists($offset, $this->elements); } /** * Offset to retrieve * * {@inheritDoc} * * @phpstan-param int $offset * * @phpstan-return T|null */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->elements[$offset] ?? null; } /** * Offset to set * * {@inheritDoc} * * @phpstan-param int|null $offset * @phpstan-param T $value */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { if ($offset === null) { $this->elements[] = $value; } else { $this->elements[$offset] = $value; } } /** * Offset to unset * * {@inheritDoc} * * @phpstan-param int $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { if (! \array_key_exists($offset, $this->elements)) { return; } unset($this->elements[$offset]); } /** * Returns a subset of the array * * @return array<int, mixed> * * @phpstan-return array<int, T> */ public function slice(int $offset, ?int $length = null): array { return \array_slice($this->elements, $offset, $length, true); } /** * @return array<int, mixed> * * @phpstan-return array<int, T> */ public function toArray(): array { return $this->elements; } } Util/LinkParserHelper.php 0000644 00000007652 15105714346 0011424 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Parser\Cursor; /** * @psalm-immutable */ final class LinkParserHelper { /** * Attempt to parse link destination * * @return string|null The string, or null if no match */ public static function parseLinkDestination(Cursor $cursor): ?string { if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) { // Chop off surrounding <..>: return UrlEncoder::unescapeAndEncode( RegexHelper::unescape(\substr($res, 1, -1)) ); } if ($cursor->getCurrentCharacter() === '<') { return null; } $destination = self::manuallyParseLinkDestination($cursor); if ($destination === null) { return null; } return UrlEncoder::unescapeAndEncode( RegexHelper::unescape($destination) ); } public static function parseLinkLabel(Cursor $cursor): int { $match = $cursor->match('/^\[(?:[^\\\\\[\]]|\\\\.){0,1000}\]/'); if ($match === null) { return 0; } $length = \mb_strlen($match, 'UTF-8'); if ($length > 1001) { return 0; } return $length; } public static function parsePartialLinkLabel(Cursor $cursor): ?string { return $cursor->match('/^(?:[^\\\\\[\]]+|\\\\.?)*/'); } /** * Attempt to parse link title (sans quotes) * * @return string|null The string, or null if no match */ public static function parseLinkTitle(Cursor $cursor): ?string { if ($title = $cursor->match('/' . RegexHelper::PARTIAL_LINK_TITLE . '/')) { // Chop off quotes from title and unescape return RegexHelper::unescape(\substr($title, 1, -1)); } return null; } public static function parsePartialLinkTitle(Cursor $cursor, string $endDelimiter): ?string { $endDelimiter = \preg_quote($endDelimiter, '/'); $regex = \sprintf('/(%s|[^%s\x00])*(?:%s)?/', RegexHelper::PARTIAL_ESCAPED_CHAR, $endDelimiter, $endDelimiter); if (($partialTitle = $cursor->match($regex)) === null) { return null; } return RegexHelper::unescape($partialTitle); } private static function manuallyParseLinkDestination(Cursor $cursor): ?string { $oldPosition = $cursor->getPosition(); $oldState = $cursor->saveState(); $openParens = 0; while (($c = $cursor->getCurrentCharacter()) !== null) { if ($c === '\\' && ($peek = $cursor->peek()) !== null && RegexHelper::isEscapable($peek)) { $cursor->advanceBy(2); } elseif ($c === '(') { $cursor->advanceBy(1); $openParens++; } elseif ($c === ')') { if ($openParens < 1) { break; } $cursor->advanceBy(1); $openParens--; } elseif (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $c)) { break; } else { $cursor->advanceBy(1); } } if ($openParens !== 0) { return null; } if ($cursor->getPosition() === $oldPosition && (! isset($c) || $c !== ')')) { return null; } $newPos = $cursor->getPosition(); $cursor->restoreState($oldState); $cursor->advanceBy($newPos - $cursor->getPosition()); return $cursor->getPreviousText(); } } Util/HtmlFilter.php 0000644 00000002674 15105714346 0010263 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\InvalidArgumentException; /** * @psalm-immutable */ final class HtmlFilter { // Return the entire string as-is public const ALLOW = 'allow'; // Escape the entire string so any HTML/JS won't be interpreted as such public const ESCAPE = 'escape'; // Return an empty string public const STRIP = 'strip'; /** * Runs the given HTML through the given filter * * @param string $html HTML input to be filtered * @param string $filter One of the HtmlFilter constants * * @return string Filtered HTML * * @throws InvalidArgumentException when an invalid $filter is given * * @psalm-pure */ public static function filter(string $html, string $filter): string { switch ($filter) { case self::STRIP: return ''; case self::ESCAPE: return \htmlspecialchars($html, \ENT_NOQUOTES); case self::ALLOW: return $html; default: throw new InvalidArgumentException(\sprintf('Invalid filter provided: "%s"', $filter)); } } } Util/HtmlElement.php 0000644 00000010054 15105714346 0010416 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; final class HtmlElement implements \Stringable { /** @psalm-readonly */ private string $tagName; /** @var array<string, string|bool> */ private array $attributes = []; /** @var \Stringable|\Stringable[]|string */ private $contents; /** @psalm-readonly */ private bool $selfClosing; /** * @param string $tagName Name of the HTML tag * @param array<string, string|string[]|bool> $attributes Array of attributes (values should be unescaped) * @param \Stringable|\Stringable[]|string|null $contents Inner contents, pre-escaped if needed * @param bool $selfClosing Whether the tag is self-closing */ public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false) { $this->tagName = $tagName; $this->selfClosing = $selfClosing; foreach ($attributes as $name => $value) { $this->setAttribute($name, $value); } $this->setContents($contents ?? ''); } /** @psalm-immutable */ public function getTagName(): string { return $this->tagName; } /** * @return array<string, string|bool> * * @psalm-immutable */ public function getAllAttributes(): array { return $this->attributes; } /** * @return string|bool|null * * @psalm-immutable */ public function getAttribute(string $key) { return $this->attributes[$key] ?? null; } /** * @param string|string[]|bool $value */ public function setAttribute(string $key, $value = true): self { if (\is_array($value)) { $this->attributes[$key] = \implode(' ', \array_unique($value)); } else { $this->attributes[$key] = $value; } return $this; } /** * @return \Stringable|\Stringable[]|string * * @psalm-immutable */ public function getContents(bool $asString = true) { if (! $asString) { return $this->contents; } return $this->getContentsAsString(); } /** * Sets the inner contents of the tag (must be pre-escaped if needed) * * @param \Stringable|\Stringable[]|string $contents * * @return $this */ public function setContents($contents): self { $this->contents = $contents ?? ''; // @phpstan-ignore-line return $this; } /** @psalm-immutable */ public function __toString(): string { $result = '<' . $this->tagName; foreach ($this->attributes as $key => $value) { if ($value === true) { $result .= ' ' . $key; } elseif ($value === false) { continue; } else { $result .= ' ' . $key . '="' . Xml::escape($value) . '"'; } } if ($this->contents !== '') { $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>'; } elseif ($this->selfClosing && $this->tagName === 'input') { $result .= '>'; } elseif ($this->selfClosing) { $result .= ' />'; } else { $result .= '></' . $this->tagName . '>'; } return $result; } /** @psalm-immutable */ private function getContentsAsString(): string { if (\is_string($this->contents)) { return $this->contents; } if (\is_array($this->contents)) { return \implode('', $this->contents); } return (string) $this->contents; } } Util/PrioritizedList.php 0000644 00000003151 15105714346 0011340 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * @internal * * @phpstan-template T * @phpstan-implements \IteratorAggregate<T> */ final class PrioritizedList implements \IteratorAggregate { /** * @var array<int, array<mixed>> * @phpstan-var array<int, array<T>> */ private array $list = []; /** * @var \Traversable<mixed>|null * @phpstan-var \Traversable<T>|null */ private ?\Traversable $optimized = null; /** * @param mixed $item * * @phpstan-param T $item */ public function add($item, int $priority): void { $this->list[$priority][] = $item; $this->optimized = null; } /** * @return \Traversable<int, mixed> * * @phpstan-return \Traversable<int, T> */ #[\ReturnTypeWillChange] public function getIterator(): \Traversable { if ($this->optimized === null) { \krsort($this->list); $sorted = []; foreach ($this->list as $group) { foreach ($group as $item) { $sorted[] = $item; } } $this->optimized = new \ArrayIterator($sorted); } return $this->optimized; } } Util/SpecReader.php 0000644 00000004200 15105714346 0010211 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\IOException; /** * Reads in a CommonMark spec document and extracts the input/output examples for testing against them */ final class SpecReader { private function __construct() { } /** * @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}> */ public static function read(string $data): iterable { // Normalize newlines for platform independence $data = \preg_replace('/\r\n?/', "\n", $data); \assert($data !== null); $data = \preg_replace('/<!-- END TESTS -->.*$/', '', $data); \assert($data !== null); \preg_match_all('/^`{32} (example ?\w*)\n([\s\S]*?)^\.\n([\s\S]*?)^`{32}$|^#{1,6} *(.*)$/m', $data, $matches, PREG_SET_ORDER); $currentSection = 'Example'; $exampleNumber = 0; foreach ($matches as $match) { if (isset($match[4])) { $currentSection = $match[4]; continue; } yield \trim($currentSection . ' #' . $exampleNumber) => [ 'input' => \str_replace('→', "\t", $match[2]), 'output' => \str_replace('→', "\t", $match[3]), 'type' => $match[1], 'section' => $currentSection, 'number' => $exampleNumber++, ]; } } /** * @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}> * * @throws IOException if the file cannot be loaded */ public static function readFile(string $filename): iterable { if (($data = \file_get_contents($filename)) === false) { throw new IOException(\sprintf('Failed to load spec from %s', $filename)); } return self::read($data); } } Util/Xml.php 0000644 00000001342 15105714346 0006740 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * Utility class for handling/generating XML and HTML * * @psalm-immutable */ final class Xml { /** * @psalm-pure */ public static function escape(string $string): string { return \str_replace(['&', '<', '>', '"'], ['&', '<', '>', '"'], $string); } } Output/RenderedContentInterface.php 0000644 00000001114 15105714346 0013464 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Output; use League\CommonMark\Node\Block\Document; interface RenderedContentInterface extends \Stringable { /** * @psalm-mutation-free */ public function getDocument(): Document; /** * @psalm-mutation-free */ public function getContent(): string; } Output/RenderedContent.php 0000644 00000001752 15105714346 0011653 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Output; use League\CommonMark\Node\Block\Document; class RenderedContent implements RenderedContentInterface, \Stringable { /** @psalm-readonly */ private Document $document; /** @psalm-readonly */ private string $content; public function __construct(Document $document, string $content) { $this->document = $document; $this->content = $content; } public function getDocument(): Document { return $this->document; } public function getContent(): string { return $this->content; } /** * @psalm-mutation-free */ public function __toString(): string { return $this->content; } } Reference/Reference.php 0000644 00000002206 15105714346 0011057 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * @psalm-immutable */ final class Reference implements ReferenceInterface { /** @psalm-readonly */ private string $label; /** @psalm-readonly */ private string $destination; /** @psalm-readonly */ private string $title; public function __construct(string $label, string $destination, string $title) { $this->label = $label; $this->destination = $destination; $this->title = $title; } public function getLabel(): string { return $this->label; } public function getDestination(): string { return $this->destination; } public function getTitle(): string { return $this->title; } } Reference/ReferenceInterface.php 0000644 00000001157 15105714346 0012704 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * Link reference */ interface ReferenceInterface { public function getLabel(): string; public function getDestination(): string; public function getTitle(): string; } Reference/ReferenceMapInterface.php 0000644 00000001423 15105714346 0013336 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * A collection of references * * @phpstan-extends \IteratorAggregate<ReferenceInterface> */ interface ReferenceMapInterface extends \IteratorAggregate, \Countable { public function add(ReferenceInterface $reference): void; public function contains(string $label): bool; public function get(string $label): ?ReferenceInterface; } Reference/ReferenceableInterface.php 0000644 00000000625 15105714346 0013527 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Reference; interface ReferenceableInterface { public function getReference(): ReferenceInterface; } Reference/ReferenceMap.php 0000644 00000003515 15105714346 0011521 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; use League\CommonMark\Normalizer\TextNormalizer; /** * A collection of references, indexed by label */ final class ReferenceMap implements ReferenceMapInterface { /** @psalm-readonly */ private TextNormalizer $normalizer; /** * @var array<string, ReferenceInterface> * * @psalm-readonly-allow-private-mutation */ private array $references = []; public function __construct() { $this->normalizer = new TextNormalizer(); } public function add(ReferenceInterface $reference): void { // Normalize the key $key = $this->normalizer->normalize($reference->getLabel()); // Store the reference $this->references[$key] = $reference; } public function contains(string $label): bool { $label = $this->normalizer->normalize($label); return isset($this->references[$label]); } public function get(string $label): ?ReferenceInterface { $label = $this->normalizer->normalize($label); return $this->references[$label] ?? null; } /** * @return \Traversable<string, ReferenceInterface> */ public function getIterator(): \Traversable { foreach ($this->references as $normalizedLabel => $reference) { yield $normalizedLabel => $reference; } } public function count(): int { return \count($this->references); } } Reference/ReferenceParser.php 0000644 00000022026 15105714346 0012236 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\LinkParserHelper; final class ReferenceParser { // Looking for the start of a definition, i.e. `[` private const START_DEFINITION = 0; // Looking for and parsing the label, i.e. `[foo]` within `[foo]` private const LABEL = 1; // Parsing the destination, i.e. `/url` in `[foo]: /url` private const DESTINATION = 2; // Looking for the start of a title, i.e. the first `"` in `[foo]: /url "title"` private const START_TITLE = 3; // Parsing the content of the title, i.e. `title` in `[foo]: /url "title"` private const TITLE = 4; // End state, no matter what kind of lines we add, they won't be references private const PARAGRAPH = 5; /** @psalm-readonly-allow-private-mutation */ private string $paragraph = ''; /** * @var array<int, ReferenceInterface> * * @psalm-readonly-allow-private-mutation */ private array $references = []; /** @psalm-readonly-allow-private-mutation */ private int $state = self::START_DEFINITION; /** @psalm-readonly-allow-private-mutation */ private ?string $label = null; /** @psalm-readonly-allow-private-mutation */ private ?string $destination = null; /** * @var string string * * @psalm-readonly-allow-private-mutation */ private string $title = ''; /** @psalm-readonly-allow-private-mutation */ private ?string $titleDelimiter = null; /** @psalm-readonly-allow-private-mutation */ private bool $referenceValid = false; public function getParagraphContent(): string { return $this->paragraph; } /** * @return ReferenceInterface[] */ public function getReferences(): iterable { $this->finishReference(); return $this->references; } public function hasReferences(): bool { return $this->references !== []; } public function parse(string $line): void { if ($this->paragraph !== '') { $this->paragraph .= "\n"; } $this->paragraph .= $line; $cursor = new Cursor($line); while (! $cursor->isAtEnd()) { $result = false; switch ($this->state) { case self::PARAGRAPH: // We're in a paragraph now. Link reference definitions can only appear at the beginning, so once // we're in a paragraph, there's no going back. return; case self::START_DEFINITION: $result = $this->parseStartDefinition($cursor); break; case self::LABEL: $result = $this->parseLabel($cursor); break; case self::DESTINATION: $result = $this->parseDestination($cursor); break; case self::START_TITLE: $result = $this->parseStartTitle($cursor); break; case self::TITLE: $result = $this->parseTitle($cursor); break; default: // this should never happen break; } if (! $result) { $this->state = self::PARAGRAPH; return; } } } private function parseStartDefinition(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd() || $cursor->getCurrentCharacter() !== '[') { return false; } $this->state = self::LABEL; $this->label = ''; $cursor->advance(); if ($cursor->isAtEnd()) { $this->label .= "\n"; } return true; } private function parseLabel(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); $partialLabel = LinkParserHelper::parsePartialLinkLabel($cursor); if ($partialLabel === null) { return false; } \assert($this->label !== null); $this->label .= $partialLabel; if ($cursor->isAtEnd()) { // label might continue on next line $this->label .= "\n"; return true; } if ($cursor->getCurrentCharacter() !== ']') { return false; } $cursor->advance(); // end of label if ($cursor->getCurrentCharacter() !== ':') { return false; } $cursor->advance(); // spec: A link label can have at most 999 characters inside the square brackets if (\mb_strlen($this->label, 'UTF-8') > 999) { return false; } // spec: A link label must contain at least one non-whitespace character if (\trim($this->label) === '') { return false; } $cursor->advanceToNextNonSpaceOrTab(); $this->state = self::DESTINATION; return true; } private function parseDestination(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); $destination = LinkParserHelper::parseLinkDestination($cursor); if ($destination === null) { return false; } $this->destination = $destination; $advanced = $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd()) { // Destination was at end of line, so this is a valid reference for sure (and maybe a title). // If not at end of line, wait for title to be valid first. $this->referenceValid = true; $this->paragraph = ''; } elseif ($advanced === 0) { // spec: The title must be separated from the link destination by whitespace return false; } $this->state = self::START_TITLE; return true; } private function parseStartTitle(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd()) { $this->state = self::START_DEFINITION; return true; } $this->titleDelimiter = null; switch ($c = $cursor->getCurrentCharacter()) { case '"': case "'": $this->titleDelimiter = $c; break; case '(': $this->titleDelimiter = ')'; break; default: // no title delimter found break; } if ($this->titleDelimiter !== null) { $this->state = self::TITLE; $cursor->advance(); if ($cursor->isAtEnd()) { $this->title .= "\n"; } } else { $this->finishReference(); // There might be another reference instead, try that for the same character. $this->state = self::START_DEFINITION; } return true; } private function parseTitle(Cursor $cursor): bool { \assert($this->titleDelimiter !== null); $title = LinkParserHelper::parsePartialLinkTitle($cursor, $this->titleDelimiter); if ($title === null) { // Invalid title, stop return false; } // Did we find the end delimiter? $endDelimiterFound = false; if (\substr($title, -1) === $this->titleDelimiter) { $endDelimiterFound = true; // Chop it off $title = \substr($title, 0, -1); } $this->title .= $title; if (! $endDelimiterFound && $cursor->isAtEnd()) { // Title still going, continue on next line $this->title .= "\n"; return true; } // We either hit the end delimiter or some extra whitespace $cursor->advanceToNextNonSpaceOrTab(); if (! $cursor->isAtEnd()) { // spec: No further non-whitespace characters may occur on the line. return false; } $this->referenceValid = true; $this->finishReference(); $this->paragraph = ''; // See if there's another definition $this->state = self::START_DEFINITION; return true; } private function finishReference(): void { if (! $this->referenceValid) { return; } /** @psalm-suppress PossiblyNullArgument -- these can't possibly be null if we're in this state */ $this->references[] = new Reference($this->label, $this->destination, $this->title); $this->label = null; $this->referenceValid = false; $this->destination = null; $this->title = ''; $this->titleDelimiter = null; } } Parser/MarkdownParserInterface.php 0000644 00000001043 15105714346 0013275 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Node\Block\Document; interface MarkdownParserInterface { /** * @throws CommonMarkException */ public function parse(string $input): Document; } Parser/MarkdownParserStateInterface.php 0000644 00000002064 15105714346 0014302 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Parser\Block\BlockContinueParserInterface; interface MarkdownParserStateInterface { /** * Returns the deepest open block parser */ public function getActiveBlockParser(): BlockContinueParserInterface; /** * Open block parser that was last matched during the continue phase. This is different from the currently active * block parser, as an unmatched block is only closed when a new block is started. */ public function getLastMatchedBlockParser(): BlockContinueParserInterface; /** * Returns the current content of the paragraph if the matched block is a paragraph. The content can be multiple * lines separated by newlines. */ public function getParagraphContent(): ?string; } Parser/InlineParserEngineInterface.php 0000644 00000001203 15105714346 0014055 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Node\Block\AbstractBlock; /** * Parser for inline content (text, links, emphasized text, etc). */ interface InlineParserEngineInterface { /** * Parse the given contents as inlines and insert them into the given block */ public function parse(string $contents, AbstractBlock $block): void; } Parser/InlineParserContext.php 0000644 00000005466 15105714346 0012472 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Delimiter\DelimiterStack; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Reference\ReferenceMapInterface; final class InlineParserContext { /** @psalm-readonly */ private AbstractBlock $container; /** @psalm-readonly */ private ReferenceMapInterface $referenceMap; /** @psalm-readonly */ private Cursor $cursor; /** @psalm-readonly */ private DelimiterStack $delimiterStack; /** * @var string[] * @psalm-var non-empty-array<string> * * @psalm-readonly-allow-private-mutation */ private array $matches; public function __construct(Cursor $contents, AbstractBlock $container, ReferenceMapInterface $referenceMap) { $this->referenceMap = $referenceMap; $this->container = $container; $this->cursor = $contents; $this->delimiterStack = new DelimiterStack(); } public function getContainer(): AbstractBlock { return $this->container; } public function getReferenceMap(): ReferenceMapInterface { return $this->referenceMap; } public function getCursor(): Cursor { return $this->cursor; } public function getDelimiterStack(): DelimiterStack { return $this->delimiterStack; } /** * @return string The full text that matched the InlineParserMatch definition */ public function getFullMatch(): string { return $this->matches[0]; } /** * @return int The length of the full match (in characters, not bytes) */ public function getFullMatchLength(): int { return \mb_strlen($this->matches[0], 'UTF-8'); } /** * @return string[] Similar to preg_match(), index 0 will contain the full match, and any other array elements will be captured sub-matches * * @psalm-return non-empty-array<string> */ public function getMatches(): array { return $this->matches; } /** * @return string[] */ public function getSubMatches(): array { return \array_slice($this->matches, 1); } /** * @param string[] $matches * * @psalm-param non-empty-array<string> $matches */ public function withMatches(array $matches): InlineParserContext { $ctx = clone $this; $ctx->matches = $matches; return $ctx; } } Parser/MarkdownParser.php 0000644 00000027575 15105714346 0011476 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional code based on commonmark-java (https://github.com/commonmark/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Event\DocumentPreParsedEvent; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Input\MarkdownInput; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Block\DocumentBlockParser; use League\CommonMark\Parser\Block\ParagraphParser; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceMap; final class MarkdownParser implements MarkdownParserInterface { /** @psalm-readonly */ private EnvironmentInterface $environment; /** @psalm-readonly-allow-private-mutation */ private int $maxNestingLevel; /** @psalm-readonly-allow-private-mutation */ private ReferenceMap $referenceMap; /** @psalm-readonly-allow-private-mutation */ private int $lineNumber = 0; /** @psalm-readonly-allow-private-mutation */ private Cursor $cursor; /** * @var array<int, BlockContinueParserInterface> * * @psalm-readonly-allow-private-mutation */ private array $activeBlockParsers = []; /** * @var array<int, BlockContinueParserWithInlinesInterface> * * @psalm-readonly-allow-private-mutation */ private array $closedBlockParsers = []; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } private function initialize(): void { $this->referenceMap = new ReferenceMap(); $this->lineNumber = 0; $this->activeBlockParsers = []; $this->closedBlockParsers = []; $this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level'); } /** * @throws CommonMarkException */ public function parse(string $input): Document { $this->initialize(); $documentParser = new DocumentBlockParser($this->referenceMap); $this->activateBlockParser($documentParser); $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input)); $this->environment->dispatch($preParsedEvent); $markdownInput = $preParsedEvent->getMarkdown(); foreach ($markdownInput->getLines() as $lineNumber => $line) { $this->lineNumber = $lineNumber; $this->parseLine($line); } // finalizeAndProcess $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber); $this->processInlines(); $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock())); return $documentParser->getBlock(); } /** * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each * line of input, then finalizing the document. */ private function parseLine(string $line): void { $this->cursor = new Cursor($line); $matches = $this->parseBlockContinuation(); if ($matches === null) { return; } $unmatchedBlocks = \count($this->activeBlockParsers) - $matches; $blockParser = $this->activeBlockParsers[$matches - 1]; $startedNewBlock = false; // Unless last matched container is a code block, try new container starts, // adding children to the last matched container: $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer(); while ($tryBlockStarts) { // this is a little performance optimization if ($this->cursor->isBlank()) { $this->cursor->advanceToEnd(); break; } if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) { break; } $blockStart = $this->findBlockStart($blockParser); if ($blockStart === null || $blockStart->isAborting()) { $this->cursor->advanceToNextNonSpaceOrTab(); break; } if (($state = $blockStart->getCursorState()) !== null) { $this->cursor->restoreState($state); } $startedNewBlock = true; // We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now. if ($unmatchedBlocks > 0) { $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1); $unmatchedBlocks = 0; } if ($blockStart->isReplaceActiveBlockParser()) { $this->prepareActiveBlockParserForReplacement(); } foreach ($blockStart->getBlockParsers() as $newBlockParser) { $blockParser = $this->addChild($newBlockParser); $tryBlockStarts = $newBlockParser->isContainer(); } } // What remains at the offset is a text line. Add the text to the appropriate block. // First check for a lazy paragraph continuation: if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) { $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } else { // finalize any blocks not matched if ($unmatchedBlocks > 0) { $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber); } if (! $blockParser->isContainer()) { $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } elseif (! $this->cursor->isBlank()) { $this->addChild(new ParagraphParser()); $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } } } private function parseBlockContinuation(): ?int { // For each containing block, try to parse the associated line start. // The document will always match, so we can skip the first block parser and start at 1 matches $matches = 1; for ($i = 1; $i < \count($this->activeBlockParsers); $i++) { $blockParser = $this->activeBlockParsers[$i]; $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser()); if ($blockContinue === null) { break; } if ($blockContinue->isFinalize()) { $this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber); return null; } if (($state = $blockContinue->getCursorState()) !== null) { $this->cursor->restoreState($state); } $matches++; } return $matches; } private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart { $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser); foreach ($this->environment->getBlockStartParsers() as $blockStartParser) { \assert($blockStartParser instanceof BlockStartParserInterface); if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) { return $result; } } return null; } private function closeBlockParsers(int $count, int $endLineNumber): void { for ($i = 0; $i < $count; $i++) { $blockParser = $this->deactivateBlockParser(); $this->finalize($blockParser, $endLineNumber); // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($blockParser instanceof BlockContinueParserWithInlinesInterface) { // Remember for inline parsing $this->closedBlockParsers[] = $blockParser; } } } /** * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings, * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference * definitions. */ private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void { if ($blockParser instanceof ParagraphParser) { $this->updateReferenceMap($blockParser->getReferences()); } $blockParser->getBlock()->setEndLine($endLineNumber); $blockParser->closeBlock(); } /** * Walk through a block & children recursively, parsing string content into inline content where appropriate. */ private function processInlines(): void { $p = new InlineParserEngine($this->environment, $this->referenceMap); foreach ($this->closedBlockParsers as $blockParser) { $blockParser->parseInlines($p); } } /** * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try * its parent, and so on til we find a block that can accept children. */ private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface { $blockParser->getBlock()->setStartLine($this->lineNumber); while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) { $this->closeBlockParsers(1, $this->lineNumber - 1); } $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock()); $this->activateBlockParser($blockParser); return $blockParser; } private function activateBlockParser(BlockContinueParserInterface $blockParser): void { $this->activeBlockParsers[] = $blockParser; } /** * @throws ParserLogicException */ private function deactivateBlockParser(): BlockContinueParserInterface { $popped = \array_pop($this->activeBlockParsers); if ($popped === null) { throw new ParserLogicException('The last block parser should not be deactivated'); } return $popped; } private function prepareActiveBlockParserForReplacement(): void { // Note that we don't want to parse inlines or finalize this block, as it's getting replaced. $old = $this->deactivateBlockParser(); if ($old instanceof ParagraphParser) { $this->updateReferenceMap($old->getReferences()); } $old->getBlock()->detach(); } /** * @param ReferenceInterface[] $references */ private function updateReferenceMap(iterable $references): void { foreach ($references as $reference) { if (! $this->referenceMap->contains($reference->getLabel())) { $this->referenceMap->add($reference); } } } /** * @throws ParserLogicException */ public function getActiveBlockParser(): BlockContinueParserInterface { $active = \end($this->activeBlockParsers); if ($active === false) { throw new ParserLogicException('No active block parsers are available'); } return $active; } } Parser/CursorState.php 0000644 00000002150 15105714346 0010773 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; /** * Encapsulates the current state of a cursor in case you need to rollback later. * * WARNING: Do not attempt to use this class for ANYTHING except for * type hinting and passing this object back into restoreState(). * The constructor, methods, and inner contents may change in any * future release without warning! * * @internal * * @psalm-immutable */ final class CursorState { /** * @var array<int, mixed> * * @psalm-readonly */ private array $state; /** * @internal * * @param array<int, mixed> $state */ public function __construct(array $state) { $this->state = $state; } /** * @internal * * @return array<int, mixed> */ public function toArray(): array { return $this->state; } } Parser/Inline/InlineParserInterface.php 0000644 00000001026 15105714346 0014150 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Parser\InlineParserContext; interface InlineParserInterface { public function getMatchDefinition(): InlineParserMatch; public function parse(InlineParserContext $inlineContext): bool; } Parser/Inline/InlineParserMatch.php 0000644 00000004272 15105714346 0013312 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Exception\InvalidArgumentException; final class InlineParserMatch { private string $regex; private bool $caseSensitive; private function __construct(string $regex, bool $caseSensitive = false) { $this->regex = $regex; $this->caseSensitive = $caseSensitive; } public function caseSensitive(): self { $this->caseSensitive = true; return $this; } /** * @internal * * @psalm-return non-empty-string */ public function getRegex(): string { return '/' . $this->regex . '/' . ($this->caseSensitive ? '' : 'i'); } /** * Match the given string (case-insensitive) */ public static function string(string $str): self { return new self(\preg_quote($str, '/')); } /** * Match any of the given strings (case-insensitive) */ public static function oneOf(string ...$str): self { return new self(\implode('|', \array_map(static fn (string $str): string => \preg_quote($str, '/'), $str))); } /** * Match a partial regular expression without starting/ending delimiters, anchors, or flags */ public static function regex(string $regex): self { return new self($regex); } public static function join(self ...$definitions): self { $regex = ''; $caseSensitive = null; foreach ($definitions as $definition) { $regex .= '(' . $definition->regex . ')'; if ($caseSensitive === null) { $caseSensitive = $definition->caseSensitive; } elseif ($caseSensitive !== $definition->caseSensitive) { throw new InvalidArgumentException('Case-sensitive and case-insensitive definitions cannot be combined'); } } return new self($regex, $caseSensitive ?? false); } } Parser/Inline/NewlineParser.php 0000644 00000003054 15105714346 0012515 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\InlineParserContext; final class NewlineParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\\n'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy(1); // Check previous inline for trailing spaces $spaces = 0; $lastInline = $inlineContext->getContainer()->lastChild(); if ($lastInline instanceof Text) { $trimmed = \rtrim($lastInline->getLiteral(), ' '); $spaces = \strlen($lastInline->getLiteral()) - \strlen($trimmed); if ($spaces) { $lastInline->setLiteral($trimmed); } } if ($spaces >= 2) { $inlineContext->getContainer()->appendChild(new Newline(Newline::HARDBREAK)); } else { $inlineContext->getContainer()->appendChild(new Newline(Newline::SOFTBREAK)); } return true; } } Parser/Block/AbstractBlockContinueParser.php 0000644 00000001663 15105714346 0015157 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; /** * Base class for a block parser * * Slightly more convenient to extend from vs. implementing the interface */ abstract class AbstractBlockContinueParser implements BlockContinueParserInterface { public function isContainer(): bool { return false; } public function canHaveLazyContinuationLines(): bool { return false; } public function canContain(AbstractBlock $childBlock): bool { return false; } public function addLine(string $line): void { } public function closeBlock(): void { } } Parser/Block/BlockContinueParserWithInlinesInterface.php 0000644 00000001151 15105714346 0017462 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\InlineParserEngineInterface; interface BlockContinueParserWithInlinesInterface extends BlockContinueParserInterface { /** * Parse any inlines inside of the current block */ public function parseInlines(InlineParserEngineInterface $inlineParser): void; } Parser/Block/BlockContinueParserInterface.php 0000644 00000003500 15105714346 0015304 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Cursor; /** * Interface for a block continuation parser * * A block continue parser can only handle a single block instance. The current block being parsed is stored within this parser and * can be returned once parsing has completed. If you need to parse multiple block continuations, instantiate a new parser for each one. */ interface BlockContinueParserInterface { /** * Return the current block being parsed by this parser */ public function getBlock(): AbstractBlock; /** * Return whether we are parsing a container block */ public function isContainer(): bool; /** * Return whether we are interested in possibly lazily parsing any subsequent lines */ public function canHaveLazyContinuationLines(): bool; /** * Determine whether the current block being parsed can contain the given child block */ public function canContain(AbstractBlock $childBlock): bool; /** * Attempt to parse the given line */ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue; /** * Add the given line of text to the current block */ public function addLine(string $line): void; /** * Close and finalize the current block */ public function closeBlock(): void; } Parser/Block/SkipLinesStartingWithLettersParser.php 0000644 00000003216 15105714346 0016544 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; /** * @internal * * This "parser" is actually a performance optimization. * * Most lines in a typical Markdown document probably won't match a block start. This is especially true for lines starting * with letters - nothing in the core CommonMark spec or our supported extensions will match those lines as blocks. Therefore, * if we can identify those lines and skip block start parsing, we can optimize performance by ~10%. * * Previously this optimization was hard-coded in the MarkdownParser but did not allow users to override this behavior. * By implementing this optimization as a block parser instead, users wanting custom blocks starting with letters * can instead register their block parser with a higher priority to ensure their parser is always called first. */ final class SkipLinesStartingWithLettersParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if (! $cursor->isIndented() && RegexHelper::isLetter($cursor->getNextNonSpaceCharacter())) { $cursor->advanceToNextNonSpaceOrTab(); return BlockStart::abort(); } return BlockStart::none(); } } Parser/Block/BlockStartParserInterface.php 0000644 00000002061 15105714346 0014616 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; /** * Interface for a block parser which identifies block starts. */ interface BlockStartParserInterface { /** * Check whether we should handle the block at the current position * * @param Cursor $cursor A cloned copy of the cursor at the current parsing location * @param MarkdownParserStateInterface $parserState Additional information about the state of the Markdown parser * * @return BlockStart|null The BlockStart that has been identified, or null if the block doesn't match here */ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart; } Parser/Block/BlockStart.php 0000644 00000005241 15105714346 0011623 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\CursorState; /** * Result object for starting parsing of a block; see static methods for constructors */ final class BlockStart { /** * @var BlockContinueParserInterface[] * * @psalm-readonly */ private array $blockParsers; /** @psalm-readonly-allow-private-mutation */ private ?CursorState $cursorState = null; /** @psalm-readonly-allow-private-mutation */ private bool $replaceActiveBlockParser = false; private bool $isAborting = false; private function __construct(BlockContinueParserInterface ...$blockParsers) { $this->blockParsers = $blockParsers; } /** * @return BlockContinueParserInterface[] */ public function getBlockParsers(): iterable { return $this->blockParsers; } public function getCursorState(): ?CursorState { return $this->cursorState; } public function isReplaceActiveBlockParser(): bool { return $this->replaceActiveBlockParser; } /** * @internal */ public function isAborting(): bool { return $this->isAborting; } /** * Signal that we want to parse at the given cursor position * * @return $this */ public function at(Cursor $cursor): self { $this->cursorState = $cursor->saveState(); return $this; } /** * Signal that we want to replace the active block parser with this one * * @return $this */ public function replaceActiveBlockParser(): self { $this->replaceActiveBlockParser = true; return $this; } /** * Signal that we cannot parse whatever is here * * @return null */ public static function none(): ?self { return null; } /** * Signal that we'd like to register the given parser(s) so they can parse the current block */ public static function of(BlockContinueParserInterface ...$blockParsers): self { return new self(...$blockParsers); } /** * Signal that the block parsing process should be aborted (no other block starts should be checked) * * @internal */ public static function abort(): self { $ret = new self(); $ret->isAborting = true; return $ret; } } Parser/Block/DocumentBlockParser.php 0000644 00000002410 15105714346 0013454 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Document; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceMapInterface; /** * Parser implementation which ensures everything is added to the root-level Document */ final class DocumentBlockParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private Document $document; public function __construct(ReferenceMapInterface $referenceMap) { $this->document = new Document($referenceMap); } public function getBlock(): Document { return $this->document; } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::at($cursor); } } Parser/Block/ParagraphParser.php 0000644 00000004273 15105714346 0012641 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\InlineParserEngineInterface; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceParser; final class ParagraphParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface { /** @psalm-readonly */ private Paragraph $block; /** @psalm-readonly */ private ReferenceParser $referenceParser; public function __construct() { $this->block = new Paragraph(); $this->referenceParser = new ReferenceParser(); } public function canHaveLazyContinuationLines(): bool { return true; } public function getBlock(): Paragraph { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { return BlockContinue::none(); } return BlockContinue::at($cursor); } public function addLine(string $line): void { $this->referenceParser->parse($line); } public function closeBlock(): void { if ($this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '') { $this->block->detach(); } } public function parseInlines(InlineParserEngineInterface $inlineParser): void { $content = $this->getContentString(); if ($content !== '') { $inlineParser->parse($content, $this->block); } } public function getContentString(): string { return $this->referenceParser->getParagraphContent(); } /** * @return ReferenceInterface[] */ public function getReferences(): iterable { return $this->referenceParser->getReferences(); } } Parser/Block/BlockContinue.php 0000644 00000003037 15105714346 0012313 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\CursorState; /** * Result object for continuing parsing of a block; see static methods for constructors. * * @psalm-immutable */ final class BlockContinue { /** @psalm-readonly */ private ?CursorState $cursorState = null; /** @psalm-readonly */ private bool $finalize; private function __construct(?CursorState $cursorState = null, bool $finalize = false) { $this->cursorState = $cursorState; $this->finalize = $finalize; } public function getCursorState(): ?CursorState { return $this->cursorState; } public function isFinalize(): bool { return $this->finalize; } /** * Signal that we cannot continue here * * @return null */ public static function none(): ?self { return null; } /** * Signal that we're continuing at the given position */ public static function at(Cursor $cursor): self { return new self($cursor->saveState(), false); } /** * Signal that we want to finalize and close the block */ public static function finished(): self { return new self(null, true); } } Parser/MarkdownParserState.php 0000644 00000003140 15105714346 0012455 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\ParagraphParser; /** * @internal You should rely on the interface instead */ final class MarkdownParserState implements MarkdownParserStateInterface { /** @psalm-readonly */ private BlockContinueParserInterface $activeBlockParser; /** @psalm-readonly */ private BlockContinueParserInterface $lastMatchedBlockParser; public function __construct(BlockContinueParserInterface $activeBlockParser, BlockContinueParserInterface $lastMatchedBlockParser) { $this->activeBlockParser = $activeBlockParser; $this->lastMatchedBlockParser = $lastMatchedBlockParser; } public function getActiveBlockParser(): BlockContinueParserInterface { return $this->activeBlockParser; } public function getLastMatchedBlockParser(): BlockContinueParserInterface { return $this->lastMatchedBlockParser; } public function getParagraphContent(): ?string { if (! $this->lastMatchedBlockParser instanceof ParagraphParser) { return null; } $paragraphParser = $this->lastMatchedBlockParser; $content = $paragraphParser->getContentString(); return $content === '' ? null : $content; } } Parser/Cursor.php 0000644 00000033673 15105714346 0010010 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\UnexpectedEncodingException; class Cursor { public const INDENT_LEVEL = 4; /** @psalm-readonly */ private string $line; /** @psalm-readonly */ private int $length; /** * @var int * * It's possible for this to be 1 char past the end, meaning we've parsed all chars and have * reached the end. In this state, any character-returning method MUST return null. */ private int $currentPosition = 0; private int $column = 0; private int $indent = 0; private int $previousPosition = 0; private ?int $nextNonSpaceCache = null; private bool $partiallyConsumedTab = false; /** * @var int|false * * @psalm-readonly */ private $lastTabPosition; /** @psalm-readonly */ private bool $isMultibyte; /** @var array<int, string> */ private array $charCache = []; /** * @param string $line The line being parsed (ASCII or UTF-8) */ public function __construct(string $line) { if (! \mb_check_encoding($line, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } $this->line = $line; $this->length = \mb_strlen($line, 'UTF-8') ?: 0; $this->isMultibyte = $this->length !== \strlen($line); $this->lastTabPosition = $this->isMultibyte ? \mb_strrpos($line, "\t", 0, 'UTF-8') : \strrpos($line, "\t"); } /** * Returns the position of the next character which is not a space (or tab) */ public function getNextNonSpacePosition(): int { if ($this->nextNonSpaceCache !== null) { return $this->nextNonSpaceCache; } if ($this->currentPosition >= $this->length) { return $this->length; } $cols = $this->column; for ($i = $this->currentPosition; $i < $this->length; $i++) { // This if-else was copied out of getCharacter() for performance reasons if ($this->isMultibyte) { $c = $this->charCache[$i] ??= \mb_substr($this->line, $i, 1, 'UTF-8'); } else { $c = $this->line[$i]; } if ($c === ' ') { $cols++; } elseif ($c === "\t") { $cols += 4 - ($cols % 4); } else { break; } } $this->indent = $cols - $this->column; return $this->nextNonSpaceCache = $i; } /** * Returns the next character which isn't a space (or tab) */ public function getNextNonSpaceCharacter(): ?string { $index = $this->getNextNonSpacePosition(); if ($index >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8'); } return $this->line[$index]; } /** * Calculates the current indent (number of spaces after current position) */ public function getIndent(): int { if ($this->nextNonSpaceCache === null) { $this->getNextNonSpacePosition(); } return $this->indent; } /** * Whether the cursor is indented to INDENT_LEVEL */ public function isIndented(): bool { if ($this->nextNonSpaceCache === null) { $this->getNextNonSpacePosition(); } return $this->indent >= self::INDENT_LEVEL; } public function getCharacter(?int $index = null): ?string { if ($index === null) { $index = $this->currentPosition; } // Index out-of-bounds, or we're at the end if ($index < 0 || $index >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8'); } return $this->line[$index]; } /** * Slightly-optimized version of getCurrent(null) */ public function getCurrentCharacter(): ?string { if ($this->currentPosition >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$this->currentPosition] ??= \mb_substr($this->line, $this->currentPosition, 1, 'UTF-8'); } return $this->line[$this->currentPosition]; } /** * Returns the next character (or null, if none) without advancing forwards */ public function peek(int $offset = 1): ?string { return $this->getCharacter($this->currentPosition + $offset); } /** * Whether the remainder is blank */ public function isBlank(): bool { return $this->nextNonSpaceCache === $this->length || $this->getNextNonSpacePosition() === $this->length; } /** * Move the cursor forwards */ public function advance(): void { $this->advanceBy(1); } /** * Move the cursor forwards * * @param int $characters Number of characters to advance by * @param bool $advanceByColumns Whether to advance by columns instead of spaces */ public function advanceBy(int $characters, bool $advanceByColumns = false): void { $this->previousPosition = $this->currentPosition; $this->nextNonSpaceCache = null; if ($this->currentPosition >= $this->length || $characters === 0) { return; } // Optimization to avoid tab handling logic if we have no tabs if ($this->lastTabPosition === false || $this->currentPosition > $this->lastTabPosition) { $length = \min($characters, $this->length - $this->currentPosition); $this->partiallyConsumedTab = false; $this->currentPosition += $length; $this->column += $length; return; } $nextFewChars = $this->isMultibyte ? \mb_substr($this->line, $this->currentPosition, $characters, 'UTF-8') : \substr($this->line, $this->currentPosition, $characters); if ($characters === 1) { $asArray = [$nextFewChars]; } elseif ($this->isMultibyte) { /** @var string[] $asArray */ $asArray = \mb_str_split($nextFewChars, 1, 'UTF-8'); } else { $asArray = \str_split($nextFewChars); } foreach ($asArray as $c) { if ($c === "\t") { $charsToTab = 4 - ($this->column % 4); if ($advanceByColumns) { $this->partiallyConsumedTab = $charsToTab > $characters; $charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab; $this->column += $charsToAdvance; $this->currentPosition += $this->partiallyConsumedTab ? 0 : 1; $characters -= $charsToAdvance; } else { $this->partiallyConsumedTab = false; $this->column += $charsToTab; $this->currentPosition++; $characters--; } } else { $this->partiallyConsumedTab = false; $this->currentPosition++; $this->column++; $characters--; } if ($characters <= 0) { break; } } } /** * Advances the cursor by a single space or tab, if present */ public function advanceBySpaceOrTab(): bool { $character = $this->getCurrentCharacter(); if ($character === ' ' || $character === "\t") { $this->advanceBy(1, true); return true; } return false; } /** * Parse zero or more space/tab characters * * @return int Number of positions moved */ public function advanceToNextNonSpaceOrTab(): int { $newPosition = $this->nextNonSpaceCache ?? $this->getNextNonSpacePosition(); if ($newPosition === $this->currentPosition) { return 0; } $this->advanceBy($newPosition - $this->currentPosition); $this->partiallyConsumedTab = false; // We've just advanced to where that non-space is, // so any subsequent calls to find the next one will // always return the current position. $this->nextNonSpaceCache = $this->currentPosition; $this->indent = 0; return $this->currentPosition - $this->previousPosition; } /** * Parse zero or more space characters, including at most one newline. * * Tab characters are not parsed with this function. * * @return int Number of positions moved */ public function advanceToNextNonSpaceOrNewline(): int { $remainder = $this->getRemainder(); // Optimization: Avoid the regex if we know there are no spaces or newlines if ($remainder === '' || ($remainder[0] !== ' ' && $remainder[0] !== "\n")) { $this->previousPosition = $this->currentPosition; return 0; } $matches = []; \preg_match('/^ *(?:\n *)?/', $remainder, $matches, \PREG_OFFSET_CAPTURE); // [0][0] contains the matched text // [0][1] contains the index of that match $increment = $matches[0][1] + \strlen($matches[0][0]); $this->advanceBy($increment); return $this->currentPosition - $this->previousPosition; } /** * Move the position to the very end of the line * * @return int The number of characters moved */ public function advanceToEnd(): int { $this->previousPosition = $this->currentPosition; $this->nextNonSpaceCache = null; $this->currentPosition = $this->length; return $this->currentPosition - $this->previousPosition; } public function getRemainder(): string { if ($this->currentPosition >= $this->length) { return ''; } $prefix = ''; $position = $this->currentPosition; if ($this->partiallyConsumedTab) { $position++; $charsToTab = 4 - ($this->column % 4); $prefix = \str_repeat(' ', $charsToTab); } $subString = $this->isMultibyte ? \mb_substr($this->line, $position, null, 'UTF-8') : \substr($this->line, $position); return $prefix . $subString; } public function getLine(): string { return $this->line; } public function isAtEnd(): bool { return $this->currentPosition >= $this->length; } /** * Try to match a regular expression * * Returns the matching text and advances to the end of that match * * @psalm-param non-empty-string $regex */ public function match(string $regex): ?string { $subject = $this->getRemainder(); if (! \preg_match($regex, $subject, $matches, \PREG_OFFSET_CAPTURE)) { return null; } // $matches[0][0] contains the matched text // $matches[0][1] contains the index of that match if ($this->isMultibyte) { // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $offset = \mb_strlen(\substr($subject, 0, $matches[0][1]), 'UTF-8'); $matchLength = \mb_strlen($matches[0][0], 'UTF-8'); } else { $offset = $matches[0][1]; $matchLength = \strlen($matches[0][0]); } // [0][0] contains the matched text // [0][1] contains the index of that match $this->advanceBy($offset + $matchLength); return $matches[0][0]; } /** * Encapsulates the current state of this cursor in case you need to rollback later. * * WARNING: Do not parse or use the return value for ANYTHING except for * passing it back into restoreState(), as the number of values and their * contents may change in any future release without warning. */ public function saveState(): CursorState { return new CursorState([ $this->currentPosition, $this->previousPosition, $this->nextNonSpaceCache, $this->indent, $this->column, $this->partiallyConsumedTab, ]); } /** * Restore the cursor to a previous state. * * Pass in the value previously obtained by calling saveState(). */ public function restoreState(CursorState $state): void { [ $this->currentPosition, $this->previousPosition, $this->nextNonSpaceCache, $this->indent, $this->column, $this->partiallyConsumedTab, ] = $state->toArray(); } public function getPosition(): int { return $this->currentPosition; } public function getPreviousText(): string { if ($this->isMultibyte) { return \mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'UTF-8'); } return \substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition); } public function getSubstring(int $start, ?int $length = null): string { if ($this->isMultibyte) { return \mb_substr($this->line, $start, $length, 'UTF-8'); } if ($length !== null) { return \substr($this->line, $start, $length); } return \substr($this->line, $start); } public function getColumn(): int { return $this->column; } } Parser/ParserLogicException.php 0000644 00000000701 15105714346 0012606 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\CommonMarkException; class ParserLogicException extends \LogicException implements CommonMarkException { } Parser/InlineParserEngine.php 0000644 00000015005 15105714346 0012241 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Inline\AdjacentTextMerger; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Reference\ReferenceMapInterface; /** * @internal */ final class InlineParserEngine implements InlineParserEngineInterface { /** @psalm-readonly */ private EnvironmentInterface $environment; /** @psalm-readonly */ private ReferenceMapInterface $referenceMap; /** * @var array<int, InlineParserInterface|string|bool> * @psalm-var list<array{0: InlineParserInterface, 1: non-empty-string, 2: bool}> * @phpstan-var array<int, array{0: InlineParserInterface, 1: non-empty-string, 2: bool}> */ private array $parsers = []; public function __construct(EnvironmentInterface $environment, ReferenceMapInterface $referenceMap) { $this->environment = $environment; $this->referenceMap = $referenceMap; foreach ($environment->getInlineParsers() as $parser) { \assert($parser instanceof InlineParserInterface); $regex = $parser->getMatchDefinition()->getRegex(); $this->parsers[] = [$parser, $regex, \strlen($regex) !== \mb_strlen($regex, 'UTF-8')]; } } public function parse(string $contents, AbstractBlock $block): void { $contents = \trim($contents); $cursor = new Cursor($contents); $inlineParserContext = new InlineParserContext($cursor, $block, $this->referenceMap); // Have all parsers look at the line to determine what they might want to parse and what positions they exist at foreach ($this->matchParsers($contents) as $matchPosition => $parsers) { $currentPosition = $cursor->getPosition(); // We've already gone past this point if ($currentPosition > $matchPosition) { continue; } // We've skipped over some uninteresting text that should be added as a plain text node if ($currentPosition < $matchPosition) { $cursor->advanceBy($matchPosition - $currentPosition); $this->addPlainText($cursor->getPreviousText(), $block); } // We're now at a potential start - see which of the current parsers can handle it $parsed = false; foreach ($parsers as [$parser, $matches]) { \assert($parser instanceof InlineParserInterface); if ($parser->parse($inlineParserContext->withMatches($matches))) { // A parser has successfully handled the text at the given position; don't consider any others at this position $parsed = true; break; } } if ($parsed) { continue; } // Despite potentially being interested, nothing actually parsed text here, so add the current character and continue onwards $this->addPlainText((string) $cursor->getCurrentCharacter(), $block); $cursor->advance(); } // Add any remaining text that wasn't parsed if (! $cursor->isAtEnd()) { $this->addPlainText($cursor->getRemainder(), $block); } // Process any delimiters that were found $delimiterStack = $inlineParserContext->getDelimiterStack(); $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors()); $delimiterStack->removeAll(); // Combine adjacent text notes into one AdjacentTextMerger::mergeChildNodes($block); } private function addPlainText(string $text, AbstractBlock $container): void { $lastInline = $container->lastChild(); if ($lastInline instanceof Text && ! $lastInline->data->has('delim')) { $lastInline->append($text); } else { $container->appendChild(new Text($text)); } } /** * Given the current line, ask all the parsers which parts of the text they would be interested in parsing. * * The resulting array provides a list of character positions, which parsers are interested in trying to parse * the text at those points, and (for convenience/optimization) what the matching text happened to be. * * @return array<array<int, InlineParserInterface|string>> * * @psalm-return array<int, list<array{0: InlineParserInterface, 1: non-empty-array<string>}>> * * @phpstan-return array<int, array<int, array{0: InlineParserInterface, 1: non-empty-array<string>}>> */ private function matchParsers(string $contents): array { $contents = \trim($contents); $isMultibyte = ! \mb_check_encoding($contents, 'ASCII'); $ret = []; foreach ($this->parsers as [$parser, $regex, $isRegexMultibyte]) { if ($isMultibyte || $isRegexMultibyte) { $regex .= 'u'; } // See if the parser's InlineParserMatch regex matched against any part of the string if (! \preg_match_all($regex, $contents, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER)) { continue; } // For each part that matched... foreach ($matches as $match) { if ($isMultibyte) { // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $offset = \mb_strlen(\substr($contents, 0, $match[0][1]), 'UTF-8'); } else { $offset = \intval($match[0][1]); } // Remove the offsets, keeping only the matched text $m = \array_column($match, 0); if ($m === []) { continue; } // Add this match to the list of character positions to stop at $ret[$offset][] = [$parser, $m]; } } // Sort matches by position so we visit them in order \ksort($ret); return $ret; } } Extension/Footnote/Parser/FootnoteStartParser.php 0000644 00000003405 15105714346 0016262 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Reference\Reference; use League\CommonMark\Util\RegexHelper; final class FootnoteStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || $parserState->getLastMatchedBlockParser()->canHaveLazyContinuationLines()) { return BlockStart::none(); } $match = RegexHelper::matchFirst( '/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition() ); if (! $match) { return BlockStart::none(); } $cursor->advanceToNextNonSpaceOrTab(); $cursor->advanceBy(\strlen($match[0])); $str = $cursor->getRemainder(); \preg_replace('/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', '', $str); if (\preg_match('/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', $match[0], $matches) !== 1) { return BlockStart::none(); } $reference = new Reference($matches[1], $matches[1], $matches[1]); $footnoteParser = new FootnoteParser($reference); return BlockStart::of($footnoteParser)->at($cursor); } } Extension/Footnote/Parser/AnonymousFootnoteRefParser.php 0000644 00000004161 15105714346 0017612 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Environment\EnvironmentAwareInterface; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationInterface; final class AnonymousFootnoteRefParser implements InlineParserInterface, EnvironmentAwareInterface { private ConfigurationInterface $config; /** @psalm-readonly-allow-private-mutation */ private TextNormalizerInterface $slugNormalizer; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\^\[([^\]]+)\]'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); [$label] = $inlineContext->getSubMatches(); $reference = $this->createReference($label); $inlineContext->getContainer()->appendChild(new FootnoteRef($reference, $label)); return true; } private function createReference(string $label): Reference { $refLabel = $this->slugNormalizer->normalize($label, ['length' => 20]); return new Reference( $refLabel, '#' . $this->config->get('footnote/footnote_id_prefix') . $refLabel, $label ); } public function setEnvironment(EnvironmentInterface $environment): void { $this->config = $environment->getConfiguration(); $this->slugNormalizer = $environment->getSlugNormalizer(); } } Extension/Footnote/Parser/FootnoteRefParser.php 0000644 00000003253 15105714346 0015702 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\[\^([^\s\]]+)\]'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); [$label] = $inlineContext->getSubMatches(); $inlineContext->getContainer()->appendChild(new FootnoteRef($this->createReference($label))); return true; } private function createReference(string $label): Reference { return new Reference( $label, '#' . $this->config->get('footnote/footnote_id_prefix') . $label, $label ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } Extension/Footnote/Parser/FootnoteParser.php 0000644 00000003404 15105714346 0015243 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceInterface; final class FootnoteParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private Footnote $block; /** @psalm-readonly-allow-private-mutation */ private ?int $indentation = null; public function __construct(ReferenceInterface $reference) { $this->block = new Footnote($reference); } public function getBlock(): Footnote { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { return BlockContinue::at($cursor); } if ($cursor->isIndented()) { $this->indentation ??= $cursor->getIndent(); $cursor->advanceBy($this->indentation, true); return BlockContinue::at($cursor); } return BlockContinue::none(); } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } } Extension/Footnote/Renderer/FootnoteRenderer.php 0000644 00000004341 15105714346 0016070 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param Footnote $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Footnote::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/footnote_class')); $attrs->set('id', $this->config->get('footnote/footnote_id_prefix') . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8')); $attrs->set('role', 'doc-endnote'); return new HtmlElement( 'li', $attrs->export(), $childRenderer->renderNodes($node->children()), true ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote'; } /** * @param Footnote $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { Footnote::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } Extension/Footnote/Renderer/FootnoteRefRenderer.php 0000644 00000004701 15105714346 0016525 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param FootnoteRef $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { FootnoteRef::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/ref_class')); $attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8')); $attrs->set('role', 'doc-noteref'); $idPrefix = $this->config->get('footnote/ref_id_prefix'); return new HtmlElement( 'sup', [ 'id' => $idPrefix . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8'), ], new HtmlElement( 'a', $attrs->export(), $node->getReference()->getTitle() ), true ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_ref'; } /** * @param FootnoteRef $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { FootnoteRef::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } Extension/Footnote/Renderer/FootnoteContainerRenderer.php 0000644 00000004101 15105714346 0017725 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteContainerRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param FootnoteContainer $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { FootnoteContainer::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/container_class')); $attrs->set('role', 'doc-endnotes'); $contents = new HtmlElement('ol', [], $childRenderer->renderNodes($node->children())); if ($this->config->get('footnote/container_add_hr')) { $contents = [new HtmlElement('hr', [], null, true), $contents]; } return new HtmlElement('div', $attrs->export(), $contents); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_container'; } /** * @return array<string, scalar> */ public function getXmlAttributes(Node $node): array { return []; } } Extension/Footnote/Renderer/FootnoteBackrefRenderer.php 0000644 00000004530 15105714346 0017346 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteBackrefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { public const DEFAULT_SYMBOL = '↩'; private ConfigurationInterface $config; /** * @param FootnoteBackref $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { FootnoteBackref::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/backref_class')); $attrs->set('rev', 'footnote'); $attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8')); $attrs->set('role', 'doc-backlink'); $symbol = $this->config->get('footnote/backref_symbol'); \assert(\is_string($symbol)); return ' ' . new HtmlElement('a', $attrs->export(), \htmlspecialchars($symbol), true); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_backref'; } /** * @param FootnoteBackref $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { FootnoteBackref::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } Extension/Footnote/FootnoteExtension.php 0000644 00000006725 15105714346 0014540 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\Footnote\Event\AnonymousFootnotesListener; use League\CommonMark\Extension\Footnote\Event\FixOrphanedFootnotesAndRefsListener; use League\CommonMark\Extension\Footnote\Event\GatherFootnotesListener; use League\CommonMark\Extension\Footnote\Event\NumberFootnotesListener; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Extension\Footnote\Parser\AnonymousFootnoteRefParser; use League\CommonMark\Extension\Footnote\Parser\FootnoteRefParser; use League\CommonMark\Extension\Footnote\Parser\FootnoteStartParser; use League\CommonMark\Extension\Footnote\Renderer\FootnoteBackrefRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteContainerRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteRefRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteRenderer; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class FootnoteExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('footnote', Expect::structure([ 'backref_class' => Expect::string('footnote-backref'), 'backref_symbol' => Expect::string('↩'), 'container_add_hr' => Expect::bool(true), 'container_class' => Expect::string('footnotes'), 'ref_class' => Expect::string('footnote-ref'), 'ref_id_prefix' => Expect::string('fnref:'), 'footnote_class' => Expect::string('footnote'), 'footnote_id_prefix' => Expect::string('fn:'), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addBlockStartParser(new FootnoteStartParser(), 51); $environment->addInlineParser(new AnonymousFootnoteRefParser(), 35); $environment->addInlineParser(new FootnoteRefParser(), 51); $environment->addRenderer(FootnoteContainer::class, new FootnoteContainerRenderer()); $environment->addRenderer(Footnote::class, new FootnoteRenderer()); $environment->addRenderer(FootnoteRef::class, new FootnoteRefRenderer()); $environment->addRenderer(FootnoteBackref::class, new FootnoteBackrefRenderer()); $environment->addEventListener(DocumentParsedEvent::class, [new AnonymousFootnotesListener(), 'onDocumentParsed'], 40); $environment->addEventListener(DocumentParsedEvent::class, [new FixOrphanedFootnotesAndRefsListener(), 'onDocumentParsed'], 30); $environment->addEventListener(DocumentParsedEvent::class, [new NumberFootnotesListener(), 'onDocumentParsed'], 20); $environment->addEventListener(DocumentParsedEvent::class, [new GatherFootnotesListener(), 'onDocumentParsed'], 10); } } Extension/Footnote/Event/NumberFootnotesListener.php 0000644 00000004446 15105714346 0016764 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Reference\Reference; final class NumberFootnotesListener { public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $nextCounter = 1; $usedLabels = []; $usedCounters = []; foreach ($document->iterator() as $node) { if (! $node instanceof FootnoteRef) { continue; } $existingReference = $node->getReference(); $label = $existingReference->getLabel(); $counter = $nextCounter; $canIncrementCounter = true; if (\array_key_exists($label, $usedLabels)) { /* * Reference is used again, we need to point * to the same footnote. But with a different ID */ $counter = $usedCounters[$label]; $label .= '__' . ++$usedLabels[$label]; $canIncrementCounter = false; } // rewrite reference title to use a numeric link $newReference = new Reference( $label, $existingReference->getDestination(), (string) $counter ); // Override reference with numeric link $node->setReference($newReference); $document->getReferenceMap()->add($newReference); /* * Store created references in document for * creating FootnoteBackrefs */ $document->data->append($existingReference->getDestination(), $newReference); $usedLabels[$label] = 1; $usedCounters[$label] = $nextCounter; if ($canIncrementCounter) { $nextCounter++; } } } } Extension/Footnote/Event/GatherFootnotesListener.php 0000644 00000006727 15105714346 0016752 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\NodeIterator; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class GatherFootnotesListener implements ConfigurationAwareInterface { private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $footnotes = []; foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if (! $node instanceof Footnote) { continue; } // Look for existing reference with footnote label $ref = $document->getReferenceMap()->get($node->getReference()->getLabel()); if ($ref !== null) { // Use numeric title to get footnotes order $footnotes[(int) $ref->getTitle()] = $node; } else { // Footnote call is missing, append footnote at the end $footnotes[\PHP_INT_MAX] = $node; } $key = '#' . $this->config->get('footnote/footnote_id_prefix') . $node->getReference()->getDestination(); if ($document->data->has($key)) { $this->createBackrefs($node, $document->data->get($key)); } } // Only add a footnote container if there are any if (\count($footnotes) === 0) { return; } $container = $this->getFootnotesContainer($document); \ksort($footnotes); foreach ($footnotes as $footnote) { $container->appendChild($footnote); } } private function getFootnotesContainer(Document $document): FootnoteContainer { $footnoteContainer = new FootnoteContainer(); $document->appendChild($footnoteContainer); return $footnoteContainer; } /** * Look for all footnote refs pointing to this footnote and create each footnote backrefs. * * @param Footnote $node The target footnote * @param Reference[] $backrefs References to create backrefs for */ private function createBackrefs(Footnote $node, array $backrefs): void { // Backrefs should be added to the child paragraph $target = $node->lastChild(); if ($target === null) { // This should never happen, but you never know $target = $node; } foreach ($backrefs as $backref) { $target->appendChild(new FootnoteBackref(new Reference( $backref->getLabel(), '#' . $this->config->get('footnote/ref_id_prefix') . $backref->getLabel(), $backref->getTitle() ))); } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } Extension/Footnote/Event/AnonymousFootnotesListener.php 0000644 00000004066 15105714346 0017522 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class AnonymousFootnotesListener implements ConfigurationAwareInterface { private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); foreach ($document->iterator() as $node) { if (! $node instanceof FootnoteRef || ($text = $node->getContent()) === null) { continue; } // Anonymous footnote needs to create a footnote from its content $existingReference = $node->getReference(); $newReference = new Reference( $existingReference->getLabel(), '#' . $this->config->get('footnote/ref_id_prefix') . $existingReference->getLabel(), $existingReference->getTitle() ); $paragraph = new Paragraph(); $paragraph->appendChild(new Text($text)); $paragraph->appendChild(new FootnoteBackref($newReference)); $footnote = new Footnote($newReference); $footnote->appendChild($paragraph); $document->appendChild($footnote); } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } Extension/Footnote/Event/FixOrphanedFootnotesAndRefsListener.php 0000644 00000004461 15105714346 0021203 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Inline\Text; final class FixOrphanedFootnotesAndRefsListener { public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $map = $this->buildMapOfKnownFootnotesAndRefs($document); foreach ($map['_flat'] as $node) { if ($node instanceof FootnoteRef && ! isset($map[Footnote::class][$node->getReference()->getLabel()])) { // Found an orphaned FootnoteRef without a corresponding Footnote // Restore the original footnote ref text $node->replaceWith(new Text(\sprintf('[^%s]', $node->getReference()->getLabel()))); } // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($node instanceof Footnote && ! isset($map[FootnoteRef::class][$node->getReference()->getLabel()])) { // Found an orphaned Footnote without a corresponding FootnoteRef // Remove the footnote $node->detach(); } } } /** @phpstan-ignore-next-line */ private function buildMapOfKnownFootnotesAndRefs(Document $document): array // @phpcs:ignore { $map = [ Footnote::class => [], FootnoteRef::class => [], '_flat' => [], ]; foreach ($document->iterator() as $node) { if ($node instanceof Footnote) { $map[Footnote::class][$node->getReference()->getLabel()] = true; $map['_flat'][] = $node; } elseif ($node instanceof FootnoteRef) { $map[FootnoteRef::class][$node->getReference()->getLabel()] = true; $map['_flat'][] = $node; } } return $map; } } Extension/Footnote/Node/FootnoteRef.php 0000644 00000002547 15105714346 0014163 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; final class FootnoteRef extends AbstractInline implements ReferenceableInterface { private ReferenceInterface $reference; /** @psalm-readonly */ private ?string $content = null; /** * @param array<mixed> $data */ public function __construct(ReferenceInterface $reference, ?string $content = null, array $data = []) { parent::__construct(); $this->reference = $reference; $this->content = $content; if (\count($data) > 0) { $this->data->import($data); } } public function getReference(): ReferenceInterface { return $this->reference; } public function setReference(ReferenceInterface $reference): void { $this->reference = $reference; } public function getContent(): ?string { return $this->content; } } Extension/Footnote/Node/Footnote.php 0000644 00000001626 15105714346 0013523 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; final class Footnote extends AbstractBlock implements ReferenceableInterface { /** @psalm-readonly */ private ReferenceInterface $reference; public function __construct(ReferenceInterface $reference) { parent::__construct(); $this->reference = $reference; } public function getReference(): ReferenceInterface { return $this->reference; } } Extension/Footnote/Node/FootnoteBackref.php 0000644 00000001766 15105714346 0015006 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; /** * Link from the footnote on the bottom of the document back to the reference */ final class FootnoteBackref extends AbstractInline implements ReferenceableInterface { /** @psalm-readonly */ private ReferenceInterface $reference; public function __construct(ReferenceInterface $reference) { parent::__construct(); $this->reference = $reference; } public function getReference(): ReferenceInterface { return $this->reference; } } Extension/Footnote/Node/FootnoteContainer.php 0000644 00000000723 15105714346 0015363 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Block\AbstractBlock; final class FootnoteContainer extends AbstractBlock { } Extension/Strikethrough/Strikethrough.php 0000644 00000001624 15105714346 0014746 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Inline\DelimitedInterface; final class Strikethrough extends AbstractInline implements DelimitedInterface { private string $delimiter; public function __construct(string $delimiter = '~~') { parent::__construct(); $this->delimiter = $delimiter; } public function getOpeningDelimiter(): string { return $this->delimiter; } public function getClosingDelimiter(): string { return $this->delimiter; } } Extension/Strikethrough/StrikethroughDelimiterProcessor.php 0000644 00000003241 15105714346 0020502 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Node\Inline\AbstractStringContainer; final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface { public function getOpeningCharacter(): string { return '~'; } public function getClosingCharacter(): string { return '~'; } public function getMinLength(): int { return 1; } public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int { if ($opener->getLength() > 2 && $closer->getLength() > 2) { return 0; } if ($opener->getLength() !== $closer->getLength()) { return 0; } return \min($opener->getLength(), $closer->getLength()); } public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void { $strikethrough = new Strikethrough(\str_repeat('~', $delimiterUse)); $tmp = $opener->next(); while ($tmp !== null && $tmp !== $closer) { $next = $tmp->next(); $strikethrough->appendChild($tmp); $tmp = $next; } $opener->insertAfter($strikethrough); } } Extension/Strikethrough/StrikethroughRenderer.php 0000644 00000002473 15105714346 0016440 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class StrikethroughRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Strikethrough $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Strikethrough::assertInstanceOf($node); return new HtmlElement('del', $node->data->get('attributes'), $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'strikethrough'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } Extension/Strikethrough/StrikethroughExtension.php 0000644 00000001447 15105714346 0016646 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ExtensionInterface; final class StrikethroughExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor()); $environment->addRenderer(Strikethrough::class, new StrikethroughRenderer()); } } Extension/ConfigurableExtensionInterface.php 0000644 00000001005 15105714346 0015351 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension; use League\Config\ConfigurationBuilderInterface; interface ConfigurableExtensionInterface extends ExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void; } Extension/ExtensionInterface.php 0000644 00000001143 15105714346 0013033 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension; use League\CommonMark\Environment\EnvironmentBuilderInterface; interface ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void; } Extension/TaskList/TaskListItemMarkerParser.php 0000644 00000003131 15105714346 0015667 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class TaskListItemMarkerParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf('[ ]', '[x]'); } public function parse(InlineParserContext $inlineContext): bool { $container = $inlineContext->getContainer(); // Checkbox must come at the beginning of the first paragraph of the list item if ($container->hasChildren() || ! ($container instanceof Paragraph && $container->parent() && $container->parent() instanceof ListItem)) { return false; } $cursor = $inlineContext->getCursor(); $oldState = $cursor->saveState(); $cursor->advanceBy(3); if ($cursor->getNextNonSpaceCharacter() === null) { $cursor->restoreState($oldState); return false; } $isChecked = $inlineContext->getFullMatch() !== '[ ]'; $container->appendChild(new TaskListItemMarker($isChecked)); return true; } } Extension/TaskList/TaskListItemMarkerRenderer.php 0000644 00000003425 15105714346 0016207 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TaskListItemMarkerRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param TaskListItemMarker $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { TaskListItemMarker::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $checkbox = new HtmlElement('input', $attrs, '', true); if ($node->isChecked()) { $checkbox->setAttribute('checked', ''); } $checkbox->setAttribute('disabled', ''); $checkbox->setAttribute('type', 'checkbox'); return $checkbox; } public function getXmlTagName(Node $node): string { return 'task_list_item_marker'; } /** * @param TaskListItemMarker $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { TaskListItemMarker::assertInstanceOf($node); if ($node->isChecked()) { return ['checked' => 'checked']; } return []; } } Extension/TaskList/TaskListItemMarker.php 0000644 00000001470 15105714346 0014516 0 ustar 00 Extension/TaskList/TaskListExtension.php 0000644 00000001371 15105714346 0014432 0 ustar 00 Extension/InlinesOnly/InlinesOnlyExtension.php 0000644 00000007203 15105714346 0015644 0 ustar 00 Extension/InlinesOnly/ChildRenderer.php 0000644 00000001665 15105714346 0014224 0 ustar 00 Extension/TableOfContents/TableOfContentsPlaceholderRenderer.php 0000644 00000002020 15105714346 0021147 0 ustar 00 Extension/TableOfContents/TableOfContentsPlaceholderParser.php 0000644 00000004747 15105714346 0020657 0 ustar 00 Extension/TableOfContents/Normalizer/FlatNormalizerStrategy.php 0000644 00000001510 15105714346 0021064 0 ustar 00 Extension/TableOfContents/Normalizer/NormalizerStrategyInterface.php 0000644 00000001006 15105714346 0022076 0 ustar 00 Extension/TableOfContents/Normalizer/RelativeNormalizerStrategy.php 0000644 00000004254 15105714346 0021761 0 ustar 00 Extension/TableOfContents/Normalizer/AsIsNormalizerStrategy.php 0000644 00000004545 15105714346 0021050 0 ustar 00 Extension/TableOfContents/TableOfContentsGeneratorInterface.php 0000644 00000001056 15105714346 0021015 0 ustar 00 Extension/TableOfContents/TableOfContentsGenerator.php 0000644 00000013670 15105714346 0017201 0 ustar 00 Extension/TableOfContents/TableOfContentsExtension.php 0000644 00000005572 15105714346 0017231 0 ustar 00 Extension/TableOfContents/TableOfContentsRenderer.php 0000644 00000002763 15105714346 0017022 0 ustar 00 Extension/TableOfContents/TableOfContentsBuilder.php 0000644 00000007364 15105714346 0016644 0 ustar 00 Extension/TableOfContents/Node/TableOfContentsPlaceholder.php 0000644 00000000677 15105714346 0020365 0 ustar 00 Extension/TableOfContents/Node/TableOfContents.php 0000644 00000000701 15105714346 0016206 0 ustar 00 Extension/ExternalLink/ExternalLinkProcessor.php 0000644 00000007303 15105714346 0016142 0 ustar 00 Extension/ExternalLink/ExternalLinkExtension.php 0000644 00000003371 15105714346 0016140 0 ustar 00 Extension/Table/TableCell.php 0000644 00000004243 15105714346 0012120 0 ustar 00 Extension/Table/Table.php 0000644 00000000752 15105714346 0011321 0 ustar 00 Extension/Table/TableRowRenderer.php 0000644 00000002670 15105714346 0013501 0 ustar 00 Extension/Table/TableSection.php 0000644 00000002441 15105714346 0012643 0 ustar 00 Extension/Table/TableRow.php 0000644 00000000755 15105714346 0012014 0 ustar 00 Extension/Table/TableRenderer.php 0000644 00000002725 15105714346 0013012 0 ustar 00 Extension/Table/TableStartParser.php 0000644 00000010725 15105714346 0013515 0 ustar 00 Extension/Table/TableExtension.php 0000644 00000004664 15105714346 0013224 0 ustar 00 Extension/Table/TableSectionRenderer.php 0000644 00000003455 15105714346 0014340 0 ustar 00 Extension/Table/TableCellRenderer.php 0000644 00000005071 15105714346 0013607 0 ustar 00 Extension/Table/TableParser.php 0000644 00000013470 15105714346 0012477 0 ustar 00 Extension/DescriptionList/Parser/DescriptionTermContinueParser.php 0000644 00000003011 15105714346 0021602 0 ustar 00 Extension/DescriptionList/Parser/DescriptionStartParser.php 0000644 00000004773 15105714346 0020303 0 ustar 00 Extension/DescriptionList/Parser/DescriptionContinueParser.php 0000644 00000003534 15105714346 0020764 0 ustar 00 Extension/DescriptionList/Parser/DescriptionListContinueParser.php 0000644 00000002760 15105714346 0021620 0 ustar 00 Extension/DescriptionList/Renderer/DescriptionTermRenderer.php 0000644 00000002042 15105714346 0020724 0 ustar 00 Extension/DescriptionList/Renderer/DescriptionListRenderer.php 0000644 00000002167 15105714346 0020740 0 ustar 00 Extension/DescriptionList/Renderer/DescriptionRenderer.php 0000644 00000002022 15105714346 0020072 0 ustar 00 Extension/DescriptionList/DescriptionListExtension.php 0000644 00000003557 15105714346 0017404 0 ustar 00 Extension/DescriptionList/Event/ConsecutiveDescriptionListMerger.php 0000644 00000002223 15105714346 0022127 0 ustar 00 Extension/DescriptionList/Event/LooseDescriptionHandler.php 0000644 00000004336 15105714346 0020230 0 ustar 00 Extension/DescriptionList/Node/DescriptionList.php 0000644 00000000656 15105714346 0016371 0 ustar 00 Extension/DescriptionList/Node/DescriptionTerm.php 0000644 00000000656 15105714346 0016365 0 ustar 00 Extension/DescriptionList/Node/Description.php 0000644 00000001503 15105714346 0015525 0 ustar 00 Extension/Mention/MentionExtension.php 0000644 00000005236 15105714346 0014164 0 ustar 00 Extension/Mention/MentionParser.php 0000644 00000005606 15105714346 0013445 0 ustar 00 Extension/Mention/Generator/CallbackGenerator.php 0000644 00000003107 15105714346 0016142 0 ustar 00 Extension/Mention/Generator/StringTemplateLinkGenerator.php 0000644 00000001516 15105714346 0020230 0 ustar 00 Extension/Mention/Generator/MentionGeneratorInterface.php 0000644 00000001036 15105714346 0017677 0 ustar 00 Extension/Mention/Mention.php 0000644 00000004002 15105714346 0012255 0 ustar 00 Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php 0000644 00000003603 15105714346 0017704 0 ustar 00 Extension/DisallowedRawHtml/DisallowedRawHtmlExtension.php 0000644 00000003315 15105714346 0020112 0 ustar 00 Extension/FrontMatter/FrontMatterParserInterface.php 0000644 00000001020 15105714346 0016740 0 ustar 00 Extension/FrontMatter/Input/MarkdownInputWithFrontMatter.php 0000644 00000002242 15105714346 0020427 0 ustar 00 Extension/FrontMatter/Output/RenderedContentWithFrontMatter.php 0000644 00000002320 15105714346 0021106 0 ustar 00 Extension/FrontMatter/Exception/InvalidFrontMatterException.php 0000644 00000001220 15105714346 0021070 0 ustar 00 Extension/FrontMatter/Data/LibYamlFrontMatterParser.php 0000644 00000002276 15105714346 0017260 0 ustar 00 Extension/FrontMatter/Data/FrontMatterDataParserInterface.php 0000644 00000001261 15105714346 0020412 0 ustar 00 Extension/FrontMatter/Data/SymfonyYamlFrontMatterParser.php 0000644 00000002141 15105714346 0020205 0 ustar 00 Extension/FrontMatter/FrontMatterProviderInterface.php 0000644 00000000677 15105714346 0017317 0 ustar 00 Extension/FrontMatter/Listener/FrontMatterPreParser.php 0000644 00000001713 15105714346 0017364 0 ustar 00 Extension/FrontMatter/Listener/FrontMatterPostRenderListener.php 0000644 00000001747 15105714346 0021263 0 ustar 00 Extension/FrontMatter/FrontMatterExtension.php 0000644 00000003372 15105714346 0015653 0 ustar 00 Extension/FrontMatter/FrontMatterParser.php 0000644 00000004447 15105714346 0015137 0 ustar 00 Extension/GithubFlavoredMarkdownExtension.php 0000644 00000002235 15105714346 0015546 0 ustar 00 Extension/Attributes/Util/AttributesHelper.php 0000644 00000010553 15105714346 0015574 0 ustar 00 Extension/Attributes/AttributesExtension.php 0000644 00000002230 15105714346 0015405 0 ustar 00 Extension/Attributes/Parser/AttributesBlockContinueParser.php 0000644 00000006271 15105714346 0020612 0 ustar 00 Extension/Attributes/Parser/AttributesInlineParser.php 0000644 00000003232 15105714346 0017263 0 ustar 00 Extension/Attributes/Parser/AttributesBlockStartParser.php 0000644 00000002513 15105714346 0020116 0 ustar 00 Extension/Attributes/Event/AttributesListener.php 0000644 00000010503 15105714346 0016301 0 ustar 00 Extension/Attributes/Node/Attributes.php 0000644 00000002567 15105714346 0014412 0 ustar 00 Extension/Attributes/Node/AttributesInline.php 0000644 00000002311 15105714346 0015534 0 ustar 00 Extension/Embed/DomainFilteringAdapter.php 0000644 00000002564 15105714346 0014636 0 ustar 00 Extension/Embed/EmbedRenderer.php 0000644 00000001502 15105714346 0012754 0 ustar 00 Extension/Embed/EmbedStartParser.php 0000644 00000003240 15105714346 0013461 0 ustar 00 Extension/Embed/Embed.php 0000644 00000001775 15105714346 0011301 0 ustar 00 Extension/Embed/EmbedAdapterInterface.php 0000644 00000001050 15105714346 0014405 0 ustar 00 Extension/Embed/EmbedParser.php 0000644 00000002470 15105714346 0012447 0 ustar 00 Extension/Embed/EmbedExtension.php 0000644 00000003345 15105714346 0013171 0 ustar 00 Extension/Embed/Bridge/OscaroteroEmbedAdapter.php 0000644 00000002636 15105714346 0016034 0 ustar 00 Extension/Embed/EmbedProcessor.php 0000644 00000004025 15105714346 0013170 0 ustar 00 Extension/DefaultAttributes/ApplyDefaultAttributesProcessor.php 0000644 00000004117 15105714346 0021236 0 ustar 00 Extension/DefaultAttributes/DefaultAttributesExtension.php 0000644 00000002375 15105714346 0020231 0 ustar 00 Extension/CommonMark/CommonMarkCoreExtension.php 0000644 00000012125 15105714346 0016054 0 ustar 00 Extension/CommonMark/Parser/Inline/HtmlInlineParser.php 0000644 00000002375 15105714346 0017203 0 ustar 00 Extension/CommonMark/Parser/Inline/EscapableParser.php 0000644 00000003153 15105714346 0017012 0 ustar 00 Extension/CommonMark/Parser/Inline/AutolinkParser.php 0000644 00000003542 15105714346 0016723 0 ustar 00 Extension/CommonMark/Parser/Inline/CloseBracketParser.php 0000644 00000016323 15105714346 0017477 0 ustar 00 Extension/CommonMark/Parser/Inline/OpenBracketParser.php 0000644 00000002541 15105714346 0017330 0 ustar 00 Extension/CommonMark/Parser/Inline/BacktickParser.php 0000644 00000004136 15105714346 0016650 0 ustar 00 Extension/CommonMark/Parser/Inline/BangParser.php 0000644 00000002544 15105714346 0016005 0 ustar 00 Extension/CommonMark/Parser/Inline/EntityParser.php 0000644 00000002421 15105714346 0016404 0 ustar 00 Extension/CommonMark/Parser/Block/ThematicBreakStartParser.php 0000644 00000002367 15105714346 0020476 0 ustar 00 Extension/CommonMark/Parser/Block/IndentedCodeStartParser.php 0000644 00000002273 15105714346 0020314 0 ustar 00 Extension/CommonMark/Parser/Block/ThematicBreakParser.php 0000644 00000002226 15105714346 0017452 0 ustar 00 Extension/CommonMark/Parser/Block/BlockQuoteStartParser.php 0000644 00000002131 15105714346 0020030 0 ustar 00 Extension/CommonMark/Parser/Block/HeadingParser.php 0000644 00000002744 15105714346 0016313 0 ustar 00 Extension/CommonMark/Parser/Block/ListBlockParser.php 0000644 00000004504 15105714346 0016636 0 ustar 00 Extension/CommonMark/Parser/Block/FencedCodeStartParser.php 0000644 00000002342 15105714346 0017743 0 ustar 00 Extension/CommonMark/Parser/Block/HtmlBlockStartParser.php 0000644 00000004126 15105714346 0017645 0 ustar 00 Extension/CommonMark/Parser/Block/ListItemParser.php 0000644 00000005573 15105714346 0016511 0 ustar 00 Extension/CommonMark/Parser/Block/FencedCodeParser.php 0000644 00000005340 15105714346 0016726 0 ustar 00 Extension/CommonMark/Parser/Block/ListBlockStartParser.php 0000644 00000012553 15105714346 0017657 0 ustar 00 Extension/CommonMark/Parser/Block/HtmlBlockParser.php 0000644 00000004372 15105714346 0016632 0 ustar 00 Extension/CommonMark/Parser/Block/HeadingStartParser.php 0000644 00000004735 15105714346 0017333 0 ustar 00 Extension/CommonMark/Parser/Block/BlockQuoteParser.php 0000644 00000003052 15105714346 0017015 0 ustar 00 Extension/CommonMark/Parser/Block/IndentedCodeParser.php 0000644 00000004242 15105714346 0017274 0 ustar 00 Extension/CommonMark/Renderer/Inline/CodeRenderer.php 0000644 00000002745 15105714346 0016637 0 ustar 00 Extension/CommonMark/Renderer/Inline/LinkRenderer.php 0000644 00000005071 15105714346 0016655 0 ustar 00 Extension/CommonMark/Renderer/Inline/EmphasisRenderer.php 0000644 00000002741 15105714346 0017532 0 ustar 00 Extension/CommonMark/Renderer/Inline/ImageRenderer.php 0000644 00000005735 15105714346 0017011 0 ustar 00 Extension/CommonMark/Renderer/Inline/HtmlInlineRenderer.php 0000644 00000003462 15105714346 0020025 0 ustar 00 Extension/CommonMark/Renderer/Inline/StrongRenderer.php 0000644 00000002737 15105714346 0017242 0 ustar 00 Extension/CommonMark/Renderer/Block/ThematicBreakRenderer.php 0000644 00000002727 15105714346 0020304 0 ustar 00 Extension/CommonMark/Renderer/Block/ListBlockRenderer.php 0000644 00000004636 15105714346 0017470 0 ustar 00 Extension/CommonMark/Renderer/Block/FencedCodeRenderer.php 0000644 00000004051 15105714346 0017550 0 ustar 00 Extension/CommonMark/Renderer/Block/IndentedCodeRenderer.php 0000644 00000003143 15105714346 0020117 0 ustar 00 Extension/CommonMark/Renderer/Block/HtmlBlockRenderer.php 0000644 00000003453 15105714346 0017455 0 ustar 00 Extension/CommonMark/Renderer/Block/BlockQuoteRenderer.php 0000644 00000003625 15105714346 0017647 0 ustar 00 Extension/CommonMark/Renderer/Block/HeadingRenderer.php 0000644 00000003300 15105714346 0017124 0 ustar 00 Extension/CommonMark/Renderer/Block/ListItemRenderer.php 0000644 00000004062 15105714346 0017325 0 ustar 00 Extension/CommonMark/Delimiter/Processor/EmphasisDelimiterProcessor.php 0000644 00000006225 15105714346 0022514 0 ustar 00 Extension/CommonMark/Node/Inline/HtmlInline.php 0000644 00000001241 15105714346 0015446 0 ustar 00 Extension/CommonMark/Node/Inline/Strong.php 0000644 00000001755 15105714346 0014671 0 ustar 00 Extension/CommonMark/Node/Inline/Emphasis.php 0000644 00000001756 15105714346 0015167 0 ustar 00 Extension/CommonMark/Node/Inline/AbstractWebResource.php 0000644 00000001557 15105714346 0017326 0 ustar 00 Extension/CommonMark/Node/Inline/Image.php 0000644 00000002125 15105714346 0014427 0 ustar 00 Extension/CommonMark/Node/Inline/Code.php 0000644 00000001066 15105714346 0014262 0 ustar 00 Extension/CommonMark/Node/Inline/Link.php 0000644 00000002124 15105714346 0014301 0 ustar 00 Extension/CommonMark/Node/Block/FencedCode.php 0000644 00000003770 15105714346 0015207 0 ustar 00 Extension/CommonMark/Node/Block/ThematicBreak.php 0000644 00000000655 15105714346 0015732 0 ustar 00 Extension/CommonMark/Node/Block/BlockQuote.php 0000644 00000000652 15105714346 0015274 0 ustar 00 Extension/CommonMark/Node/Block/ListBlock.php 0000644 00000002416 15105714346 0015112 0 ustar 00 Extension/CommonMark/Node/Block/ListData.php 0000644 00000002070 15105714346 0014725 0 ustar 00 Extension/CommonMark/Node/Block/IndentedCode.php 0000644 00000001350 15105714346 0015545 0 ustar 00 Extension/CommonMark/Node/Block/HtmlBlock.php 0000644 00000003447 15105714346 0015110 0 ustar 00 Extension/CommonMark/Node/Block/ListItem.php 0000644 00000001500 15105714346 0014747 0 ustar 00 Extension/CommonMark/Node/Block/Heading.php 0000644 00000001542 15105714346 0014562 0 ustar 00 Extension/Autolink/AutolinkExtension.php 0000644 00000001320 15105714346 0014504 0 ustar 00 Extension/Autolink/EmailAutolinkParser.php 0000644 00000002651 15105714346 0014744 0 ustar 00 Extension/Autolink/UrlAutolinkParser.php 0000644 00000015752 15105714346 0014465 0 ustar 00 Extension/HeadingPermalink/HeadingPermalinkRenderer.php 0000644 00000005741 15105714346 0017341 0 ustar 00 Extension/HeadingPermalink/HeadingPermalink.php 0000644 00000001346 15105714346 0015647 0 ustar 00 Extension/HeadingPermalink/HeadingPermalinkProcessor.php 0000644 00000007353 15105714346 0017553 0 ustar 00 Extension/HeadingPermalink/HeadingPermalinkExtension.php 0000644 00000004123 15105714346 0017540 0 ustar 00 Extension/SmartPunct/SmartPunctExtension.php 0000644 00000005200 15105714346 0015331 0 ustar 00 Extension/SmartPunct/DashParser.php 0000644 00000003562 15105714346 0013401 0 ustar 00 Extension/SmartPunct/EllipsesParser.php 0000644 00000002117 15105714346 0014275 0 ustar 00 Extension/SmartPunct/Quote.php 0000644 00000001501 15105714346 0012431 0 ustar 00 Extension/SmartPunct/ReplaceUnpairedQuotesListener.php 0000644 00000002462 15105714346 0017315 0 ustar 00 Extension/SmartPunct/QuoteProcessor.php 0000644 00000004473 15105714346 0014344 0 ustar 00 Extension/SmartPunct/QuoteParser.php 0000644 00000006702 15105714346 0013616 0 ustar 00 Exception/LogicException.php 0000644 00000000610 15105714346 0012132 0 ustar 00 Exception/CommonMarkException.php 0000644 00000000664 15105714346 0013151 0 ustar 00 Exception/IOException.php 0000644 00000000607 15105714346 0011412 0 ustar 00 Exception/AlreadyInitializedException.php 0000644 00000000624 15105714346 0014651 0 ustar 00 Exception/MissingDependencyException.php 0000644 00000000626 15105714346 0014514 0 ustar 00 Exception/UnexpectedEncodingException.php 0000644 00000000635 15105714346 0014657 0 ustar 00 Renderer/HtmlRenderer.php 0000644 00000005465 15105714346 0011436 0 ustar 00 Renderer/HtmlDecorator.php 0000644 00000002357 15105714346 0011607 0 ustar 00 Renderer/DocumentRendererInterface.php 0000644 00000001257 15105714346 0014124 0 ustar 00 Renderer/Inline/TextRenderer.php 0000644 00000002473 15105714346 0012670 0 ustar 00 Renderer/Inline/NewlineRenderer.php 0000644 00000003734 15105714346 0013346 0 ustar 00 Renderer/NodeRendererInterface.php 0000644 00000001226 15105714346 0013227 0 ustar 00 Renderer/Block/ParagraphRenderer.php 0000644 00000003672 15105714346 0013467 0 ustar 00 Renderer/Block/DocumentRenderer.php 0000644 00000002673 15105714346 0013340 0 ustar 00 Renderer/ChildNodeRendererInterface.php 0000644 00000001275 15105714346 0014177 0 ustar 00 Renderer/NoMatchingRendererException.php 0000644 00000000645 15105714346 0014433 0 ustar 00 Renderer/MarkdownRendererInterface.php 0000644 00000001330 15105714346 0014120 0 ustar 00 Xml/FallbackNodeXmlRenderer.php 0000644 00000004256 15105714346 0012507 0 ustar 00 Xml/MarkdownToXmlConverter.php 0000644 00000003104 15105714346 0012457 0 ustar 00 Xml/XmlRenderer.php 0000644 00000010106 15105714346 0010250 0 ustar 00 Xml/XmlNodeRendererInterface.php 0000644 00000001142 15105714346 0012677 0 ustar 00 Delimiter/DelimiterStack.php 0000644 00000016771 15105714346 0012121 0 ustar 00 Delimiter/DelimiterInterface.php 0000644 00000002303 15105714346 0012736 0 ustar 00 Delimiter/Delimiter.php 0000644 00000005740 15105714346 0011125 0 ustar 00 Delimiter/Processor/DelimiterProcessorCollection.php 0000644 00000005551 15105714346 0017020 0 ustar 00 Delimiter/Processor/DelimiterProcessorCollectionInterface.php 0000644 00000002652 15105714346 0020640 0 ustar 00 Delimiter/Processor/DelimiterProcessorInterface.php 0000644 00000005417 15105714346 0016626 0 ustar 00 Delimiter/Processor/StaggeredDelimiterProcessor.php 0000644 00000007023 15105714346 0016626 0 ustar 00 Delimiter/DelimiterParser.php 0000644 00000007160 15105714346 0012300 0 ustar 00 ConverterInterface.php 0000644 00000001423 15105714346 0011053 0 ustar 00 Event/DocumentPreRenderEvent.php 0000644 00000001616 15105714346 0012737 0 ustar 00 Event/DocumentParsedEvent.php 0000644 00000001330 15105714346 0012260 0 ustar 00 Event/DocumentPreParsedEvent.php 0000644 00000002164 15105714346 0012735 0 ustar 00 Event/DocumentRenderedEvent.php 0000644 00000001604 15105714346 0012576 0 ustar 00 2013-6420 applied. return self::$useOpensslParse = true; } if (defined('PHP_WINDOWS_VERSION_BUILD')) { // Windows is probably insecure in this case. return self::$useOpensslParse = false; } $compareDistroVersionPrefix = function ($prefix, $fixedVersion) { $regex = '{^'.preg_quote($prefix).'([0-9]+)$}'; if (preg_match($regex, PHP_VERSION, $m)) { return ((int) $m[1]) >= $fixedVersion; } return false; }; // Hard coded list of PHP distributions with the fix backported. if ( $compareDistroVersionPrefix('5.3.3-7+squeeze', 18) // Debian 6 (Squeeze) || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy) || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise) Event/ListenerData.php 0000644 00000001605 15105714346 0010725 0 ustar 00 ates the bug. // Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415 // changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593 $cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEV Event/AbstractEvent.php 0000644 00000002752 15105714346 0011117 0 ustar 00 TjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'; $script = <<<'EOT' error_reporting(-1); $info = openssl_x509_parse(base64_decode('%s')); var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']); EOT; $script = '<'."?php\n".sprintf($script, $cert); try { $process = new PhpProcess($script); $process->mustRun(); } catch (\Exception $e) { // In the case of any exceptions just accept it is not possible to // determine the safety of openssl_x509_parse and bail out. return self::$useOpensslParse = false; CommonMarkConverter.php 0000644 00000002236 15105714346 0011221 0 ustar 00 ine \d+}', $errorOutput) ) { // This PHP has the fix backported probably by a distro security team. return self::$useOpensslParse = true; } return self::$useOpensslParse = false; } /** * Resets the static caches * @return void */ public static function reset() { self::$caFileValidity = array(); self::$caPath = null; self::$useOpensslParse = null; } /** * @param string $name * @return string|false */ private static function getEnvVariable($name) { if (isset($_SERVER[$name])) { return (string) $_SERVER[$name]; } if (PHP_SAPI === 'cli' && ($value = getenv($name)) !== false && $value !== null) { return (string) $value; } return false; } /** * @param string|false $certFile * @param LoggerInterface|null $logger * @return bool */ private static function caFileUsable($certFile, LoggerInterface $logger = null) { return $certFile && static::isFile($certFile, $logger) && static::isReadable($certFile, $log GithubFlavoredMarkdownConverter.php 0000644 00000002255 15105714346 0013567 0 ustar 00 isFile; } /** * @param string $certDir * @param LoggerInterface|null $logger * @return bool */ private static function isDir($certDir, LoggerInterface $logger = null) { $isDir = @is_dir($certDir); if (!$isDir && $logger) { $logger->debug(sprintf('Checked directory %s does not exist or it is not a directory.', $certDir)); } return $isDir; } /** * @param string $certFileOrDir * @param LoggerInterface|null $logger * @return bool */ private static function isReadable($certFileOrDir, LoggerInterface $logger = null) { $isReadable = @is_readable($certFileOrDir); if (!$isReadable && $logger) { $logger->debug(sprintf('Checked file or directory %s is not readable.', $certFileOrDir)); } return $isReadable; } /** * @param string $pattern * @param LoggerInterface|null $logger * @return bool */ private static function glob($pattern, LoggerInterface $logger = null) { $certs = glob($pattern); if ($certs === false) { if ($logger) { $logger->debug(sp Node/RawMarkupContainerInterface.php 0000644 00000000721 15105714346 0013545 0 ustar 00 Node/StringContainerHelper.php 0000644 00000002514 15105714346 0012423 0 ustar 00 Node/NodeIterator.php 0000644 00000002542 15105714346 0010552 0 ustar 00 Node/Query.php 0000644 00000006360 15105714346 0007262 0 ustar 00 Node/StringContainerInterface.php 0000644 00000001200 15105714346 0013073 0 ustar 00 Node/Inline/AbstractStringContainer.php 0000644 00000002127 15105714346 0014165 0 ustar 00 Node/Inline/DelimitedInterface.php 0000644 00000000702 15105714346 0013106 0 ustar 00 Node/Inline/AbstractInline.php 0000644 00000001007 15105714346 0012266 0 ustar 00 Node/Inline/Newline.php 0000644 00000001641 15105714346 0010771 0 ustar 00 Node/Inline/Text.php 0000644 00000001115 15105714346 0010310 0 ustar 00 Node/Inline/AdjacentTextMerger.php 0000644 00000005604 15105714346 0013113 0 ustar 00 Node/NodeWalker.php 0000644 00000004231 15105714346 0010203 0 ustar 00 Node/Query/AndExpr.php 0000644 00000002156 15105714346 0010622 0 ustar 00 Node/Query/ExpressionInterface.php 0000644 00000000655 15105714346 0013243 0 ustar 00 Node/Query/OrExpr.php 0000644 00000002153 15105714346 0010475 0 ustar 00 Node/Block/Paragraph.php 0000644 00000000737 15105714346 0011116 0 ustar 00 Node/Block/AbstractBlock.php 0000644 00000002661 15105714346 0011725 0 ustar 00 Node/Block/TightBlockInterface.php 0000644 00000000662 15105714346 0013061 0 ustar 00 Node/Block/Document.php 0000644 00000002402 15105714346 0010756 0 ustar 00 Node/NodeWalkerEvent.php 0000644 00000001575 15105714346 0011215 0 ustar 00 FunctionUnit.php 0000644 00000001002 15107307253 0007676 0 ustar 00 ClassMethodUnit.php 0000644 00000001013 15107307253 0010321 0 ustar 00 CodeUnitCollection.php 0000644 00000003035 15107307253 0011007 0 ustar 00 FileUnit.php 0000644 00000000766 15107307253 0007010 0 ustar 00 CodeUnit.php 0000644 00000030063 15107307253 0006774 0 ustar 00 TraitUnit.php 0000644 00000000771 15107307254 0007211 0 ustar 00 InterfaceMethodUnit.php 0000644 00000001023 15107307254 0011156 0 ustar 00 exceptions/Exception.php 0000644 00000000560 15107307254 0011401 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of phpunit/php-text-template. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use Throwable; interface Exception extends Throwable { } exceptions/NoTraitException.php 0000644 00000000626 15107307254 0012705 0 ustar 00 exceptions/InvalidCodeUnitException.php 0000644 00000000636 15107307254 0014347 0 ustar 00 exceptions/ReflectionException.php 0000644 00000000631 15107307255 0013414 0 ustar 00 InterfaceUnit.php 0000644 00000001005 15107307255 0010016 0 ustar 00 ClassUnit.php 0000644 00000000771 15107307255 0007174 0 ustar 00 Mapper.php 0000644 00000016327 15107307255 0006517 0 ustar 00 TraitMethodUnit.php 0000644 00000001013 15107307255 0010341 0 ustar 00 error_log 0000644 00000001272 15107307255 0006470 0 ustar 00 CodeUnitCollectionIterator.php 0000644 00000002125 15107307256 0012523 0 ustar 00 Property/KeyframeSelector.php 0000644 00000001271 15107313622 0012346 0 ustar 00 Property/Charset.php 0000644 00000004440 15107313622 0010474 0 ustar 00 Property/Import.php 0000644 00000004760 15107313623 0010363 0 ustar 00 Property/CSSNamespace.php 0000644 00000005247 15107313623 0011357 0 ustar 00 Property/Selector.php 0000644 00000006553 15107313623 0010673 0 ustar 00 Property/AtRule.php 0000644 00000001441 15107313623 0010276 0 ustar 00 Renderable.php 0000644 00000000450 15107313623 0007320 0 ustar 00 Parsing/SourceException.php 0000644 00000001062 15107313623 0011777 0 ustar 00 Parsing/ParserState.php 0000644 00000032212 15107313623 0011116 0 ustar 00 Parsing/UnexpectedTokenException.php 0000644 00000002704 15107313623 0013650 0 ustar 00 Parsing/OutputException.php 0000644 00000000546 15107313624 0012046 0 ustar 00 Parsing/UnexpectedEOFException.php 0000644 00000000421 15107313624 0013174 0 ustar 00 Settings.php 0000644 00000003647 15107313624 0007071 0 ustar 00 Comment/Commentable.php 0000644 00000000704 15107313624 0011110 0 ustar 00 Comment/Comment.php 0000644 00000002215 15107313624 0010263 0 ustar 00 RuleSet/AtRuleSet.php 0000644 00000002620 15107313625 0010513 0 ustar 00 RuleSet/RuleSet.php 0000644 00000025324 15107313625 0010234 0 ustar 00 RuleSet/DeclarationBlock.php 0000644 00000071606 15107313625 0012055 0 ustar 00 OutputFormat.php 0000644 00000017233 15107313626 0007740 0 ustar 00 Value/RuleValueList.php 0000644 00000000443 15107313626 0011076 0 ustar 00 Value/CalcFunction.php 0000644 00000006520 15107313626 0010710 0 ustar 00 Value/CalcRuleValueList.php 0000644 00000000671 15107313627 0011665 0 ustar 00 Value/Value.php 0000644 00000016041 15107313627 0007414 0 ustar 00 * @param array<array-key, string> $aListDelimiters * * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) { /** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */ $aStack = []; $oParserState->consumeWhiteSpace(); //Build a list of delimiters and parsed values while ( !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\')) ) { if (count($aStack) > 0) { $bFoundDelimiter = false; foreach ($aListDelimiters as $sDelimiter) { if ($oParserState->comes($sDelimiter)) { array_push($aStack, $oParserState->consume($sDelimiter)); $oParserState->consumeWhiteSpace(); $bFoundDelimiter = true; break; } } if (!$bFoundDelimiter) { //Whitespace was the list delimiter array_push($aStack, ' '); } } array_push($aStack, self::parsePrimitiveValue($oParserState)); $oParserState->consumeWhiteSpace(); } // Convert the list to list objects foreach ($aListDelimiters as $sDelimiter) { if (count($aStack) === 1) { return $aStack[0]; } $iStartPosition = null; while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { $iLength = 2; //Number of elements to be joined for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) { if ($sDelimiter !== $aStack[$i]) { break; } } $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { $oList->addListComponent($aStack[$i]); } array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); } } if (!isset($aStack[0])) { throw new UnexpectedTokenException( " {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine() ); } return $aStack[0]; } /** * @param bool $bIgnoreCase * * @return CSSFunction|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { $sResult = $oParserState->parseIdentifier($bIgnoreCase); if ($oParserState->comes('(')) { $oParserState->consume('('); $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); } return $sResult; } /** * @return CSSFunction|CSSString|LineName|Size|URL|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException * @throws SourceException */ public static function parsePrimitiveValue(ParserState $oParserState) { $oValue = null; $oParserState->consumeWhiteSpace(); if ( is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1))) ) { $oValue = Size::parse($oParserState); } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { $oValue = Color::parse($oParserState); } elseif ($oParserState->comes('url', true)) { $oValue = URL::parse($oParserState); } elseif ( $oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true) ) { $oValue = CalcFunction::parse($oParserState); } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { $oValue = CSSString::parse($oParserState); } elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { $oValue = self::parseMicrosoftFilter($oParserState); } elseif ($oParserState->comes("[")) { $oValue = LineName::parse($oParserState); } elseif ($oParserState->comes("U+")) { $oValue = self::parseUnicodeRangeValue($oParserState); } else { $oValue = self::parseIdentifierOrFunction($oParserState); } $oParserState->consumeWhiteSpace(); return $oValue; } /** * @return CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseMicrosoftFilter(ParserState $oParserState) { $sFunction = $oParserState->consumeUntil('(', false, true); $aArguments = Value::parseValue($oParserState, [',', '=']); return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); } /** * @return string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseUnicodeRangeValue(ParserState $oParserState) { $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits $sRange = ""; $oParserState->consume("U+"); do { if ($oParserState->comes('-')) { $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them } $sRange .= $oParserState->consume(1); } while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); return "U+{$sRange}"; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } } Value/PrimitiveValue.php 0000644 00000000344 15107313627 0011304 0 ustar 00 <?php namespace Sabberworm\CSS\Value; abstract class PrimitiveValue extends Value { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } } Value/ValueList.php 0000644 00000004536 15107313627 0010256 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; abstract class ValueList extends Value { /** * @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ protected $aComponents; /** * @var string */ protected $sSeparator; /** * phpcs:ignore Generic.Files.LineLength * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents * @param string $sSeparator * @param int $iLineNo */ public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) { parent::__construct($iLineNo); if (!is_array($aComponents)) { $aComponents = [$aComponents]; } $this->aComponents = $aComponents; $this->sSeparator = $sSeparator; } /** * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent * * @return void */ public function addListComponent($mComponent) { $this->aComponents[] = $mComponent; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getListComponents() { return $this->aComponents; } /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents * * @return void */ public function setListComponents(array $aComponents) { $this->aComponents = $aComponents; } /** * @return string */ public function getListSeparator() { return $this->sSeparator; } /** * @param string $sSeparator * * @return void */ public function setListSeparator($sSeparator) { $this->sSeparator = $sSeparator; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return $oOutputFormat->implode( $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents ); } } Value/LineName.php 0000644 00000003411 15107313627 0010025 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; class LineName extends ValueList { /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents * @param int $iLineNo */ public function __construct(array $aComponents = [], $iLineNo = 0) { parent::__construct($aComponents, ' ', $iLineNo); } /** * @return LineName * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parse(ParserState $oParserState) { $oParserState->consume('['); $oParserState->consumeWhiteSpace(); $aNames = []; do { if ($oParserState->getSettings()->bLenientParsing) { try { $aNames[] = $oParserState->parseIdentifier(); } catch (UnexpectedTokenException $e) { if (!$oParserState->comes(']')) { throw $e; } } } else { $aNames[] = $oParserState->parseIdentifier(); } $oParserState->consumeWhiteSpace(); } while (!$oParserState->comes(']')); $oParserState->consume(']'); return new LineName($aNames, $oParserState->currentLine()); } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } } Value/CSSFunction.php 0000644 00000003066 15107313630 0010473 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; class CSSFunction extends ValueList { /** * @var string */ protected $sName; /** * @param string $sName * @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments * @param string $sSeparator * @param int $iLineNo */ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) { if ($aArguments instanceof RuleValueList) { $sSeparator = $aArguments->getListSeparator(); $aArguments = $aArguments->getListComponents(); } $this->sName = $sName; $this->iLineNo = $iLineNo; parent::__construct($aArguments, $sSeparator, $iLineNo); } /** * @return string */ public function getName() { return $this->sName; } /** * @param string $sName * * @return void */ public function setName($sName) { $this->sName = $sName; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getArguments() { return $this->aComponents; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $aArguments = parent::render($oOutputFormat); return "{$this->sName}({$aArguments})"; } } Value/Color.php 0000644 00000013230 15107313630 0007405 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; class Color extends CSSFunction { /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor * @param int $iLineNo */ public function __construct(array $aColor, $iLineNo = 0) { parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); } /** * @return Color|CSSFunction * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $aColor = []; if ($oParserState->comes('#')) { $oParserState->consume('#'); $sValue = $oParserState->parseIdentifier(false); if ($oParserState->strlen($sValue) === 3) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; } elseif ($oParserState->strlen($sValue) === 4) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3]; } if ($oParserState->strlen($sValue) === 8) { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), 'a' => new Size( round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine() ), ]; } else { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), ]; } } else { $sColorMode = $oParserState->parseIdentifier(true); $oParserState->consumeWhiteSpace(); $oParserState->consume('('); $bContainsVar = false; $iLength = $oParserState->strlen($sColorMode); for ($i = 0; $i < $iLength; ++$i) { $oParserState->consumeWhiteSpace(); if ($oParserState->comes('var')) { $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState); $bContainsVar = true; } else { $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); } if ($bContainsVar && $oParserState->comes(')')) { // With a var argument the function can have fewer arguments break; } $oParserState->consumeWhiteSpace(); if ($i < ($iLength - 1)) { $oParserState->consume(','); } } $oParserState->consume(')'); if ($bContainsVar) { return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); } } return new Color($aColor, $oParserState->currentLine()); } /** * @param float $fVal * @param float $fFromMin * @param float $fFromMax * @param float $fToMin * @param float $fToMax * * @return float */ private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) { $fFromRange = $fFromMax - $fFromMin; $fToRange = $fToMax - $fToMin; $fMultiplier = $fToRange / $fFromRange; $fNewVal = $fVal - $fFromMin; $fNewVal *= $fMultiplier; return $fNewVal + $fToMin; } /** * @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> */ public function getColor() { return $this->aComponents; } /** * @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor * * @return void */ public function setColor(array $aColor) { $this->setName(implode('', array_keys($aColor))); $this->aComponents = $aColor; } /** * @return string */ public function getColorDescription() { return $this->getName(); } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { // Shorthand RGB color values if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { $sResult = sprintf( '%02x%02x%02x', $this->aComponents['r']->getSize(), $this->aComponents['g']->getSize(), $this->aComponents['b']->getSize() ); return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); } return parent::render($oOutputFormat); } } Value/Size.php 0000644 00000012400 15107313630 0007237 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; class Size extends PrimitiveValue { /** * vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) * * @var array<int, string> */ const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; /** * @var array<int, string> */ const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** * @var array<int, string> */ const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz']; /** * @var array<int, array<string, string>>|null */ private static $SIZE_UNITS = null; /** * @var float */ private $fSize; /** * @var string|null */ private $sUnit; /** * @var bool */ private $bIsColorComponent; /** * @param float|int|string $fSize * @param string|null $sUnit * @param bool $bIsColorComponent * @param int $iLineNo */ public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { parent::__construct($iLineNo); $this->fSize = (float)$fSize; $this->sUnit = $sUnit; $this->bIsColorComponent = $bIsColorComponent; } /** * @param bool $bIsColorComponent * * @return Size * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState, $bIsColorComponent = false) { $sSize = ''; if ($oParserState->comes('-')) { $sSize .= $oParserState->consume('-'); } while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { if ($oParserState->comes('.')) { $sSize .= $oParserState->consume('.'); } else { $sSize .= $oParserState->consume(1); } } $sUnit = null; $aSizeUnits = self::getSizeUnits(); foreach ($aSizeUnits as $iLength => &$aValues) { $sKey = strtolower($oParserState->peek($iLength)); if (array_key_exists($sKey, $aValues)) { if (($sUnit = $aValues[$sKey]) !== null) { $oParserState->consume($iLength); break; } } } return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); } /** * @return array<int, array<string, string>> */ private static function getSizeUnits() { if (!is_array(self::$SIZE_UNITS)) { self::$SIZE_UNITS = []; foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { $iSize = strlen($val); if (!isset(self::$SIZE_UNITS[$iSize])) { self::$SIZE_UNITS[$iSize] = []; } self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; } krsort(self::$SIZE_UNITS, SORT_NUMERIC); } return self::$SIZE_UNITS; } /** * @param string $sUnit * * @return void */ public function setUnit($sUnit) { $this->sUnit = $sUnit; } /** * @return string|null */ public function getUnit() { return $this->sUnit; } /** * @param float|int|string $fSize */ public function setSize($fSize) { $this->fSize = (float)$fSize; } /** * @return float */ public function getSize() { return $this->fSize; } /** * @return bool */ public function isColorComponent() { return $this->bIsColorComponent; } /** * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). * * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. */ public function isSize() { if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { return false; } return !$this->isColorComponent(); } /** * @return bool */ public function isRelative() { if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { return true; } if ($this->sUnit === null && $this->fSize != 0) { return true; } return false; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $l = localeconv(); $sPoint = preg_quote($l['decimal_point'], '/'); $sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : $this->fSize; return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize) . ($this->sUnit === null ? '' : $this->sUnit); } } Value/CSSString.php 0000644 00000005247 15107313630 0010157 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; class CSSString extends PrimitiveValue { /** * @var string */ private $sString; /** * @param string $sString * @param int $iLineNo */ public function __construct($sString, $iLineNo = 0) { $this->sString = $sString; parent::__construct($iLineNo); } /** * @return CSSString * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $sBegin = $oParserState->peek(); $sQuote = null; if ($sBegin === "'") { $sQuote = "'"; } elseif ($sBegin === '"') { $sQuote = '"'; } if ($sQuote !== null) { $oParserState->consume($sQuote); } $sResult = ""; $sContent = null; if ($sQuote === null) { // Unquoted strings end in whitespace or with braces, brackets, parentheses while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { $sResult .= $oParserState->parseCharacter(false); } } else { while (!$oParserState->comes($sQuote)) { $sContent = $oParserState->parseCharacter(false); if ($sContent === null) { throw new SourceException( "Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine() ); } $sResult .= $sContent; } $oParserState->consume($sQuote); } return new CSSString($sResult, $oParserState->currentLine()); } /** * @param string $sString * * @return void */ public function setString($sString) { $this->sString = $sString; } /** * @return string */ public function getString() { return $this->sString; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sString = addslashes($this->sString); $sString = str_replace("\n", '\A', $sString); return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); } } Value/URL.php 0000644 00000003415 15107313630 0006775 0 ustar 00 <?php namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; class URL extends PrimitiveValue { /** * @var CSSString */ private $oURL; /** * @param int $iLineNo */ public function __construct(CSSString $oURL, $iLineNo = 0) { parent::__construct($iLineNo); $this->oURL = $oURL; } /** * @return URL * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $bUseUrl = $oParserState->comes('url', true); if ($bUseUrl) { $oParserState->consume('url'); $oParserState->consumeWhiteSpace(); $oParserState->consume('('); } $oParserState->consumeWhiteSpace(); $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); if ($bUseUrl) { $oParserState->consumeWhiteSpace(); $oParserState->consume(')'); } return $oResult; } /** * @return void */ public function setURL(CSSString $oURL) { $this->oURL = $oURL; } /** * @return CSSString */ public function getURL() { return $this->oURL; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { return "url({$this->oURL->render($oOutputFormat)})"; } } Parser.php 0000644 00000002457 15107313631 0006521 0 ustar 00 <?php namespace Sabberworm\CSS; use Sabberworm\CSS\CSSList\Document; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; /** * This class parses CSS from text into a data structure. */ class Parser { /** * @var ParserState */ private $oParserState; /** * @param string $sText * @param Settings|null $oParserSettings * @param int $iLineNo the line number (starting from 1, not from 0) */ public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) { if ($oParserSettings === null) { $oParserSettings = Settings::create(); } $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); } /** * @param string $sCharset * * @return void */ public function setCharset($sCharset) { $this->oParserState->setCharset($sCharset); } /** * @return void */ public function getCharset() { // Note: The `return` statement is missing here. This is a bug that needs to be fixed. $this->oParserState->getCharset(); } /** * @return Document * * @throws SourceException */ public function parse() { return Document::parse($this->oParserState); } } OutputFormatter.php 0000644 00000012336 15107313631 0010446 0 ustar 00 <?php namespace Sabberworm\CSS; use Sabberworm\CSS\Parsing\OutputException; class OutputFormatter { /** * @var OutputFormat */ private $oFormat; public function __construct(OutputFormat $oFormat) { $this->oFormat = $oFormat; } /** * @param string $sName * @param string|null $sType * * @return string */ public function space($sName, $sType = null) { $sSpaceString = $this->oFormat->get("Space$sName"); // If $sSpaceString is an array, we have multiple values configured // depending on the type of object the space applies to if (is_array($sSpaceString)) { if ($sType !== null && isset($sSpaceString[$sType])) { $sSpaceString = $sSpaceString[$sType]; } else { $sSpaceString = reset($sSpaceString); } } return $this->prepareSpace($sSpaceString); } /** * @return string */ public function spaceAfterRuleName() { return $this->space('AfterRuleName'); } /** * @return string */ public function spaceBeforeRules() { return $this->space('BeforeRules'); } /** * @return string */ public function spaceAfterRules() { return $this->space('AfterRules'); } /** * @return string */ public function spaceBetweenRules() { return $this->space('BetweenRules'); } /** * @return string */ public function spaceBeforeBlocks() { return $this->space('BeforeBlocks'); } /** * @return string */ public function spaceAfterBlocks() { return $this->space('AfterBlocks'); } /** * @return string */ public function spaceBetweenBlocks() { return $this->space('BetweenBlocks'); } /** * @return string */ public function spaceBeforeSelectorSeparator() { return $this->space('BeforeSelectorSeparator'); } /** * @return string */ public function spaceAfterSelectorSeparator() { return $this->space('AfterSelectorSeparator'); } /** * @param string $sSeparator * * @return string */ public function spaceBeforeListArgumentSeparator($sSeparator) { return $this->space('BeforeListArgumentSeparator', $sSeparator); } /** * @param string $sSeparator * * @return string */ public function spaceAfterListArgumentSeparator($sSeparator) { return $this->space('AfterListArgumentSeparator', $sSeparator); } /** * @return string */ public function spaceBeforeOpeningBrace() { return $this->space('BeforeOpeningBrace'); } /** * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. * * @param string $cCode the name of the function to call * * @return string|null */ public function safely($cCode) { if ($this->oFormat->get('IgnoreExceptions')) { // If output exceptions are ignored, run the code with exception guards try { return $cCode(); } catch (OutputException $e) { return null; } // Do nothing } else { // Run the code as-is return $cCode(); } } /** * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. * * @param string $sSeparator * @param array<array-key, Renderable|string> $aValues * @param bool $bIncreaseLevel * * @return string */ public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) { $sResult = ''; $oFormat = $this->oFormat; if ($bIncreaseLevel) { $oFormat = $oFormat->nextLevel(); } $bIsFirst = true; foreach ($aValues as $mValue) { if ($bIsFirst) { $bIsFirst = false; } else { $sResult .= $sSeparator; } if ($mValue instanceof Renderable) { $sResult .= $mValue->render($oFormat); } else { $sResult .= $mValue; } } return $sResult; } /** * @param string $sString * * @return string */ public function removeLastSemicolon($sString) { if ($this->oFormat->get('SemicolonAfterLastRule')) { return $sString; } $sString = explode(';', $sString); if (count($sString) < 2) { return $sString[0]; } $sLast = array_pop($sString); $sNextToLast = array_pop($sString); array_push($sString, $sNextToLast . $sLast); return implode(';', $sString); } /** * @param string $sSpaceString * * @return string */ private function prepareSpace($sSpaceString) { return str_replace("\n", "\n" . $this->indent(), $sSpaceString); } /** * @return string */ private function indent() { return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); } } CSSList/KeyFrame.php 0000644 00000003616 15107313631 0010252 0 ustar 00 <?php namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Property\AtRule; class KeyFrame extends CSSList implements AtRule { /** * @var string|null */ private $vendorKeyFrame; /** * @var string|null */ private $animationName; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); $this->vendorKeyFrame = null; $this->animationName = null; } /** * @param string $vendorKeyFrame */ public function setVendorKeyFrame($vendorKeyFrame) { $this->vendorKeyFrame = $vendorKeyFrame; } /** * @return string|null */ public function getVendorKeyFrame() { return $this->vendorKeyFrame; } /** * @param string $animationName */ public function setAnimationName($animationName) { $this->animationName = $animationName; } /** * @return string|null */ public function getAnimationName() { return $this->animationName; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; return $sResult; } /** * @return bool */ public function isRootList() { return false; } /** * @return string|null */ public function atRuleName() { return $this->vendorKeyFrame; } /** * @return string|null */ public function atRuleArgs() { return $this->animationName; } } CSSList/CSSList.php 0000644 00000036416 15107313631 0010037 0 ustar 00 <?php namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\Commentable; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\AtRule; use Sabberworm\CSS\Property\Charset; use Sabberworm\CSS\Property\CSSNamespace; use Sabberworm\CSS\Property\Import; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\RuleSet\AtRuleSet; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Settings; use Sabberworm\CSS\Value\CSSString; use Sabberworm\CSS\Value\URL; use Sabberworm\CSS\Value\Value; /** * A `CSSList` is the most generic container available. Its contents include `RuleSet` as well as other `CSSList` * objects. * * Also, it may contain `Import` and `Charset` objects stemming from at-rules. */ abstract class CSSList implements Renderable, Commentable { /** * @var array<array-key, Comment> */ protected $aComments; /** * @var array<int, RuleSet|CSSList|Import|Charset> */ protected $aContents; /** * @var int */ protected $iLineNo; /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { $this->aComments = []; $this->aContents = []; $this->iLineNo = $iLineNo; } /** * @return void * * @throws UnexpectedTokenException * @throws SourceException */ public static function parseList(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if (is_string($oParserState)) { $oParserState = new ParserState($oParserState, Settings::create()); } $bLenientParsing = $oParserState->getSettings()->bLenientParsing; while (!$oParserState->isEnd()) { $comments = $oParserState->consumeWhiteSpace(); $oListItem = null; if ($bLenientParsing) { try { $oListItem = self::parseListItem($oParserState, $oList); } catch (UnexpectedTokenException $e) { $oListItem = false; } } else { $oListItem = self::parseListItem($oParserState, $oList); } if ($oListItem === null) { // List parsing finished return; } if ($oListItem) { $oListItem->setComments($comments); $oList->append($oListItem); } $oParserState->consumeWhiteSpace(); } if (!$bIsRoot && !$bLenientParsing) { throw new SourceException("Unexpected end of document", $oParserState->currentLine()); } } /** * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ private static function parseListItem(ParserState $oParserState, CSSList $oList) { $bIsRoot = $oList instanceof Document; if ($oParserState->comes('@')) { $oAtRule = self::parseAtRule($oParserState); if ($oAtRule instanceof Charset) { if (!$bIsRoot) { throw new UnexpectedTokenException( '@charset may only occur in root document', '', 'custom', $oParserState->currentLine() ); } if (count($oList->getContents()) > 0) { throw new UnexpectedTokenException( '@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine() ); } $oParserState->setCharset($oAtRule->getCharset()->getString()); } return $oAtRule; } elseif ($oParserState->comes('}')) { if (!$oParserState->getSettings()->bLenientParsing) { throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine()); } else { if ($bIsRoot) { if ($oParserState->getSettings()->bLenientParsing) { return DeclarationBlock::parse($oParserState); } else { throw new SourceException("Unopened {", $oParserState->currentLine()); } } else { return null; } } } else { return DeclarationBlock::parse($oParserState, $oList); } } /** * @param ParserState $oParserState * * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null * * @throws SourceException * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ private static function parseAtRule(ParserState $oParserState) { $oParserState->consume('@'); $sIdentifier = $oParserState->parseIdentifier(); $iIdentifierLineNum = $oParserState->currentLine(); $oParserState->consumeWhiteSpace(); if ($sIdentifier === 'import') { $oLocation = URL::parse($oParserState); $oParserState->consumeWhiteSpace(); $sMediaQuery = null; if (!$oParserState->comes(';')) { $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF])); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); } elseif ($sIdentifier === 'charset') { $sCharset = CSSString::parse($oParserState); $oParserState->consumeWhiteSpace(); $oParserState->consumeUntil([';', ParserState::EOF], true, true); return new Charset($sCharset, $iIdentifierLineNum); } elseif (self::identifierIs($sIdentifier, 'keyframes')) { $oResult = new KeyFrame($iIdentifierLineNum); $oResult->setVendorKeyFrame($sIdentifier); $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); CSSList::parseList($oParserState, $oResult); if ($oParserState->comes('}')) { $oParserState->consume('}'); } return $oResult; } elseif ($sIdentifier === 'namespace') { $sPrefix = null; $mUrl = Value::parsePrimitiveValue($oParserState); if (!$oParserState->comes(';')) { $sPrefix = $mUrl; $mUrl = Value::parsePrimitiveValue($oParserState); } $oParserState->consumeUntil([';', ParserState::EOF], true, true); if ($sPrefix !== null && !is_string($sPrefix)) { throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); } if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { throw new UnexpectedTokenException( 'Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum ); } return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); } else { // Unknown other at rule (font-face or such) $sArgs = trim($oParserState->consumeUntil('{', false, true)); if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { if ($oParserState->getSettings()->bLenientParsing) { return null; } else { throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); } } $bUseRuleSet = true; foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { if (self::identifierIs($sIdentifier, $sBlockRuleName)) { $bUseRuleSet = false; break; } } if ($bUseRuleSet) { $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); RuleSet::parseRuleSet($oParserState, $oAtRule); } else { $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); CSSList::parseList($oParserState, $oAtRule); if ($oParserState->comes('}')) { $oParserState->consume('}'); } } return $oAtRule; } } /** * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. * We need to check for these versions too. * * @param string $sIdentifier * @param string $sMatch * * @return bool */ private static function identifierIs($sIdentifier, $sMatch) { return (strcasecmp($sIdentifier, $sMatch) === 0) ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * Prepends an item to the list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function prepend($oItem) { array_unshift($this->aContents, $oItem); } /** * Appends an item to tje list of contents. * * @param RuleSet|CSSList|Import|Charset $oItem * * @return void */ public function append($oItem) { $this->aContents[] = $oItem; } /** * Splices the list of contents. * * @param int $iOffset * @param int $iLength * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement * * @return void */ public function splice($iOffset, $iLength = null, $mReplacement = null) { array_splice($this->aContents, $iOffset, $iLength, $mReplacement); } /** * Removes an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oItemToRemove * May be a RuleSet (most likely a DeclarationBlock), a Import, * a Charset or another CSSList (most likely a MediaQuery) * * @return bool whether the item was removed */ public function remove($oItemToRemove) { $iKey = array_search($oItemToRemove, $this->aContents, true); if ($iKey !== false) { unset($this->aContents[$iKey]); return true; } return false; } /** * Replaces an item from the CSS list. * * @param RuleSet|Import|Charset|CSSList $oOldItem * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` * or another `CSSList` (most likely a `MediaQuery`) * * @return bool */ public function replace($oOldItem, $mNewItem) { $iKey = array_search($oOldItem, $this->aContents, true); if ($iKey !== false) { if (is_array($mNewItem)) { array_splice($this->aContents, $iKey, 1, $mNewItem); } else { array_splice($this->aContents, $iKey, 1, [$mNewItem]); } return true; } return false; } /** * @param array<int, RuleSet|Import|Charset|CSSList> $aContents */ public function setContents(array $aContents) { $this->aContents = []; foreach ($aContents as $content) { $this->append($content); } } /** * Removes a declaration block from the CSS list if it matches all given selectors. * * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks * * @return void */ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { if ($mSelector instanceof DeclarationBlock) { $mSelector = $mSelector->getSelectors(); } if (!is_array($mSelector)) { $mSelector = explode(',', $mSelector); } foreach ($mSelector as $iKey => &$mSel) { if (!($mSel instanceof Selector)) { if (!Selector::isValid($mSel)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, "custom" ); } $mSel = new Selector($mSel); } } foreach ($this->aContents as $iKey => $mItem) { if (!($mItem instanceof DeclarationBlock)) { continue; } if ($mItem->getSelectors() == $mSelector) { unset($this->aContents[$iKey]); if (!$bRemoveAll) { return; } } } } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = ''; $bIsFirst = true; $oNextLevel = $oOutputFormat; if (!$this->isRootList()) { $oNextLevel = $oOutputFormat->nextLevel(); } foreach ($this->aContents as $oContent) { $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { return $oContent->render($oNextLevel); }); if ($sRendered === null) { continue; } if ($bIsFirst) { $bIsFirst = false; $sResult .= $oNextLevel->spaceBeforeBlocks(); } else { $sResult .= $oNextLevel->spaceBetweenBlocks(); } $sResult .= $sRendered; } if (!$bIsFirst) { // Had some output $sResult .= $oOutputFormat->spaceAfterBlocks(); } return $sResult; } /** * Return true if the list can not be further outdented. Only important when rendering. * * @return bool */ abstract public function isRootList(); /** * @return array<int, RuleSet|Import|Charset|CSSList> */ public function getContents() { return $this->aContents; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } CSSList/AtRuleBlockList.php 0000644 00000003172 15107313631 0011547 0 ustar 00 <?php namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Property\AtRule; /** * A `BlockList` constructed by an unknown at-rule. `@media` rules are rendered into `AtRuleBlockList` objects. */ class AtRuleBlockList extends CSSBlockList implements AtRule { /** * @var string */ private $sType; /** * @var string */ private $sArgs; /** * @param string $sType * @param string $sArgs * @param int $iLineNo */ public function __construct($sType, $sArgs = '', $iLineNo = 0) { parent::__construct($iLineNo); $this->sType = $sType; $this->sArgs = $sArgs; } /** * @return string */ public function atRuleName() { return $this->sType; } /** * @return string */ public function atRuleArgs() { return $this->sArgs; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sArgs = $this->sArgs; if ($sArgs) { $sArgs = ' ' . $sArgs; } $sResult = $oOutputFormat->sBeforeAtRuleBlock; $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; $sResult .= parent::render($oOutputFormat); $sResult .= '}'; $sResult .= $oOutputFormat->sAfterAtRuleBlock; return $sResult; } /** * @return bool */ public function isRootList() { return false; } } CSSList/Document.php 0000644 00000011211 15107313632 0010314 0 ustar 00 <?php namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Value\Value; /** * The root `CSSList` of a parsed file. Contains all top-level CSS contents, mostly declaration blocks, * but also any at-rules encountered. */ class Document extends CSSBlockList { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } /** * @return Document * * @throws SourceException */ public static function parse(ParserState $oParserState) { $oDocument = new Document($oParserState->currentLine()); CSSList::parseList($oParserState, $oDocument); return $oDocument; } /** * Gets all `DeclarationBlock` objects recursively. * * @return array<int, DeclarationBlock> */ public function getAllDeclarationBlocks() { /** @var array<int, DeclarationBlock> $aResult */ $aResult = []; $this->allDeclarationBlocks($aResult); return $aResult; } /** * Gets all `DeclarationBlock` objects recursively. * * @return array<int, DeclarationBlock> * * @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead */ public function getAllSelectors() { return $this->getAllDeclarationBlocks(); } /** * Returns all `RuleSet` objects found recursively in the tree. * * @return array<int, RuleSet> */ public function getAllRuleSets() { /** @var array<int, RuleSet> $aResult */ $aResult = []; $this->allRuleSets($aResult); return $aResult; } /** * Returns all `Value` objects found recursively in the tree. * * @param CSSList|RuleSet|string $mElement * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). * If a string is given, it is used as rule name filter. * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. * * @return array<int, Value> * * @see RuleSet->getRules() */ public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { $sSearchString = null; if ($mElement === null) { $mElement = $this; } elseif (is_string($mElement)) { $sSearchString = $mElement; $mElement = $this; } /** @var array<int, Value> $aResult */ $aResult = []; $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); return $aResult; } /** * Returns all `Selector` objects found recursively in the tree. * * Note that this does not yield the full `DeclarationBlock` that the selector belongs to * (and, currently, there is no way to get to that). * * @param string|null $sSpecificitySearch * An optional filter by specificity. * May contain a comparison operator and a number or just a number (defaults to "=="). * * @return array<int, Selector> * @example `getSelectorsBySpecificity('>= 100')` * */ public function getSelectorsBySpecificity($sSpecificitySearch = null) { /** @var array<int, Selector> $aResult */ $aResult = []; $this->allSelectors($aResult, $sSpecificitySearch); return $aResult; } /** * Expands all shorthand properties to their long value. * * @return void */ public function expandShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandShorthands(); } } /** * Create shorthands properties whenever possible. * * @return void */ public function createShorthands() { foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createShorthands(); } } /** * Overrides `render()` to make format argument optional. * * @param OutputFormat|null $oOutputFormat * * @return string */ public function render(OutputFormat $oOutputFormat = null) { if ($oOutputFormat === null) { $oOutputFormat = new OutputFormat(); } return parent::render($oOutputFormat); } /** * @return bool */ public function isRootList() { return true; } } CSSList/CSSBlockList.php 0000644 00000012164 15107313632 0011005 0 ustar 00 <?php namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Value\CSSFunction; use Sabberworm\CSS\Value\Value; use Sabberworm\CSS\Value\ValueList; /** * A `CSSBlockList` is a `CSSList` whose `DeclarationBlock`s are guaranteed to contain valid declaration blocks or * at-rules. * * Most `CSSList`s conform to this category but some at-rules (such as `@keyframes`) do not. */ abstract class CSSBlockList extends CSSList { /** * @param int $iLineNo */ public function __construct($iLineNo = 0) { parent::__construct($iLineNo); } /** * @param array<int, DeclarationBlock> $aResult * * @return void */ protected function allDeclarationBlocks(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof DeclarationBlock) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allDeclarationBlocks($aResult); } } } /** * @param array<int, RuleSet> $aResult * * @return void */ protected function allRuleSets(array &$aResult) { foreach ($this->aContents as $mContent) { if ($mContent instanceof RuleSet) { $aResult[] = $mContent; } elseif ($mContent instanceof CSSBlockList) { $mContent->allRuleSets($aResult); } } } /** * @param CSSList|Rule|RuleSet|Value $oElement * @param array<int, Value> $aResult * @param string|null $sSearchString * @param bool $bSearchInFunctionArguments * * @return void */ protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { if ($oElement instanceof CSSBlockList) { foreach ($oElement->getContents() as $oContent) { $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof RuleSet) { foreach ($oElement->getRules($sSearchString) as $oRule) { $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); } } elseif ($oElement instanceof Rule) { $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); } elseif ($oElement instanceof ValueList) { if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { foreach ($oElement->getListComponents() as $mComponent) { $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); } } } else { // Non-List `Value` or `CSSString` (CSS identifier) $aResult[] = $oElement; } } /** * @param array<int, Selector> $aResult * @param string|null $sSpecificitySearch * * @return void */ protected function allSelectors(array &$aResult, $sSpecificitySearch = null) { /** @var array<int, DeclarationBlock> $aDeclarationBlocks */ $aDeclarationBlocks = []; $this->allDeclarationBlocks($aDeclarationBlocks); foreach ($aDeclarationBlocks as $oBlock) { foreach ($oBlock->getSelectors() as $oSelector) { if ($sSpecificitySearch === null) { $aResult[] = $oSelector; } else { $sComparator = '==='; $aSpecificitySearch = explode(' ', $sSpecificitySearch); $iTargetSpecificity = $aSpecificitySearch[0]; if (count($aSpecificitySearch) > 1) { $sComparator = $aSpecificitySearch[0]; $iTargetSpecificity = $aSpecificitySearch[1]; } $iTargetSpecificity = (int)$iTargetSpecificity; $iSelectorSpecificity = $oSelector->getSpecificity(); $bMatches = false; switch ($sComparator) { case '<=': $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; break; case '<': $bMatches = $iSelectorSpecificity < $iTargetSpecificity; break; case '>=': $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; break; case '>': $bMatches = $iSelectorSpecificity > $iTargetSpecificity; break; default: $bMatches = $iSelectorSpecificity === $iTargetSpecificity; break; } if ($bMatches) { $aResult[] = $oSelector; } } } } } } Rule/Rule.php 0000644 00000023660 15107313632 0007103 0 ustar 00 <?php namespace Sabberworm\CSS\Rule; use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\Commentable; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Value\RuleValueList; use Sabberworm\CSS\Value\Value; /** * RuleSets contains Rule objects which always have a key and a value. * In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” */ class Rule implements Renderable, Commentable { /** * @var string */ private $sRule; /** * @var RuleValueList|null */ private $mValue; /** * @var bool */ private $bIsImportant; /** * @var array<int, int> */ private $aIeHack; /** * @var int */ protected $iLineNo; /** * @var int */ protected $iColNo; /** * @var array<array-key, Comment> */ protected $aComments; /** * @param string $sRule * @param int $iLineNo * @param int $iColNo */ public function __construct($sRule, $iLineNo = 0, $iColNo = 0) { $this->sRule = $sRule; $this->mValue = null; $this->bIsImportant = false; $this->aIeHack = []; $this->iLineNo = $iLineNo; $this->iColNo = $iColNo; $this->aComments = []; } /** * @return Rule * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public static function parse(ParserState $oParserState) { $aComments = $oParserState->consumeWhiteSpace(); $oRule = new Rule( $oParserState->parseIdentifier(!$oParserState->comes("--")), $oParserState->currentLine(), $oParserState->currentColumn() ); $oRule->setComments($aComments); $oRule->addComments($oParserState->consumeWhiteSpace()); $oParserState->consume(':'); $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); $oRule->setValue($oValue); if ($oParserState->getSettings()->bLenientParsing) { while ($oParserState->comes('\\')) { $oParserState->consume('\\'); $oRule->addIeHack($oParserState->consume()); $oParserState->consumeWhiteSpace(); } } $oParserState->consumeWhiteSpace(); if ($oParserState->comes('!')) { $oParserState->consume('!'); $oParserState->consumeWhiteSpace(); $oParserState->consume('important'); $oRule->setIsImportant(true); } $oParserState->consumeWhiteSpace(); while ($oParserState->comes(';')) { $oParserState->consume(';'); } $oParserState->consumeWhiteSpace(); return $oRule; } /** * @param string $sRule * * @return array<int, string> */ private static function listDelimiterForRule($sRule) { if (preg_match('/^font($|-)/', $sRule)) { return [',', '/', ' ']; } return [',', ' ', '/']; } /** * @return int */ public function getLineNo() { return $this->iLineNo; } /** * @return int */ public function getColNo() { return $this->iColNo; } /** * @param int $iLine * @param int $iColumn * * @return void */ public function setPosition($iLine, $iColumn) { $this->iColNo = $iColumn; $this->iLineNo = $iLine; } /** * @param string $sRule * * @return void */ public function setRule($sRule) { $this->sRule = $sRule; } /** * @return string */ public function getRule() { return $this->sRule; } /** * @return RuleValueList|null */ public function getValue() { return $this->mValue; } /** * @param RuleValueList|null $mValue * * @return void */ public function setValue($mValue) { $this->mValue = $mValue; } /** * @param array<array-key, array<array-key, RuleValueList>> $aSpaceSeparatedValues * * @return RuleValueList * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. * Use `setValue()` instead and wrap the value inside a RuleValueList if necessary. */ public function setValues(array $aSpaceSeparatedValues) { $oSpaceSeparatedList = null; if (count($aSpaceSeparatedValues) > 1) { $oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo); } foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { $oCommaSeparatedList = null; if (count($aCommaSeparatedValues) > 1) { $oCommaSeparatedList = new RuleValueList(',', $this->iLineNo); } foreach ($aCommaSeparatedValues as $mValue) { if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { $this->mValue = $mValue; return $mValue; } if ($oCommaSeparatedList) { $oCommaSeparatedList->addListComponent($mValue); } else { $oSpaceSeparatedList->addListComponent($mValue); } } if (!$oSpaceSeparatedList) { $this->mValue = $oCommaSeparatedList; return $oCommaSeparatedList; } else { $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); } } $this->mValue = $oSpaceSeparatedList; return $oSpaceSeparatedList; } /** * @return array<int, array<int, RuleValueList>> * * @deprecated will be removed in version 9.0 * Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. * Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s). */ public function getValues() { if (!$this->mValue instanceof RuleValueList) { return [[$this->mValue]]; } if ($this->mValue->getListSeparator() === ',') { return [$this->mValue->getListComponents()]; } $aResult = []; foreach ($this->mValue->getListComponents() as $mValue) { if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { $aResult[] = [$mValue]; continue; } if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { $aResult[] = []; } foreach ($mValue->getListComponents() as $mValue) { $aResult[count($aResult) - 1][] = $mValue; } } return $aResult; } /** * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. * Otherwise, the existing value will be wrapped by one. * * @param RuleValueList|array<int, RuleValueList> $mValue * @param string $sType * * @return void */ public function addValue($mValue, $sType = ' ') { if (!is_array($mValue)) { $mValue = [$mValue]; } if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { $mCurrentValue = $this->mValue; $this->mValue = new RuleValueList($sType, $this->iLineNo); if ($mCurrentValue) { $this->mValue->addListComponent($mCurrentValue); } } foreach ($mValue as $mValueItem) { $this->mValue->addListComponent($mValueItem); } } /** * @param int $iModifier * * @return void */ public function addIeHack($iModifier) { $this->aIeHack[] = $iModifier; } /** * @param array<int, int> $aModifiers * * @return void */ public function setIeHack(array $aModifiers) { $this->aIeHack = $aModifiers; } /** * @return array<int, int> */ public function getIeHack() { return $this->aIeHack; } /** * @param bool $bIsImportant * * @return void */ public function setIsImportant($bIsImportant) { $this->bIsImportant = $bIsImportant; } /** * @return bool */ public function getIsImportant() { return $this->bIsImportant; } /** * @return string */ public function __toString() { return $this->render(new OutputFormat()); } /** * @return string */ public function render(OutputFormat $oOutputFormat) { $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; if ($this->mValue instanceof Value) { //Can also be a ValueList $sResult .= $this->mValue->render($oOutputFormat); } else { $sResult .= $this->mValue; } if (!empty($this->aIeHack)) { $sResult .= ' \\' . implode('\\', $this->aIeHack); } if ($this->bIsImportant) { $sResult .= ' !important'; } $sResult .= ';'; return $sResult; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function addComments(array $aComments) { $this->aComments = array_merge($this->aComments, $aComments); } /** * @return array<array-key, Comment> */ public function getComments() { return $this->aComments; } /** * @param array<array-key, Comment> $aComments * * @return void */ public function setComments(array $aComments) { $this->aComments = $aComments; } } Exception/Exception.php 0000644 00000000561 15107320450 0011150 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use Throwable; interface Exception extends Throwable { } Exception/NegativeValueException.php 0000644 00000000663 15107320450 0013633 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use InvalidArgumentException; final class NegativeValueException extends InvalidArgumentException implements Exception { } Exception/RuntimeException.php 0000644 00000000607 15107320450 0012515 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; final class RuntimeException extends \RuntimeException implements Exception { } Exception/IllogicalValuesException.php 0000644 00000000641 15107320450 0014147 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use LogicException; final class IllogicalValuesException extends LogicException implements Exception { } LinesOfCode.php 0000644 00000006536 15107320450 0007416 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; /** * @psalm-immutable */ final class LinesOfCode { /** * @psalm-var non-negative-int */ private readonly int $linesOfCode; /** * @psalm-var non-negative-int */ private readonly int $commentLinesOfCode; /** * @psalm-var non-negative-int */ private readonly int $nonCommentLinesOfCode; /** * @psalm-var non-negative-int */ private readonly int $logicalLinesOfCode; /** * @psalm-param non-negative-int $linesOfCode * @psalm-param non-negative-int $commentLinesOfCode * @psalm-param non-negative-int $nonCommentLinesOfCode * @psalm-param non-negative-int $logicalLinesOfCode * * @throws IllogicalValuesException * @throws NegativeValueException */ public function __construct(int $linesOfCode, int $commentLinesOfCode, int $nonCommentLinesOfCode, int $logicalLinesOfCode) { /** @psalm-suppress DocblockTypeContradiction */ if ($linesOfCode < 0) { throw new NegativeValueException('$linesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($commentLinesOfCode < 0) { throw new NegativeValueException('$commentLinesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($nonCommentLinesOfCode < 0) { throw new NegativeValueException('$nonCommentLinesOfCode must not be negative'); } /** @psalm-suppress DocblockTypeContradiction */ if ($logicalLinesOfCode < 0) { throw new NegativeValueException('$logicalLinesOfCode must not be negative'); } if ($linesOfCode - $commentLinesOfCode !== $nonCommentLinesOfCode) { throw new IllogicalValuesException('$linesOfCode !== $commentLinesOfCode + $nonCommentLinesOfCode'); } $this->linesOfCode = $linesOfCode; $this->commentLinesOfCode = $commentLinesOfCode; $this->nonCommentLinesOfCode = $nonCommentLinesOfCode; $this->logicalLinesOfCode = $logicalLinesOfCode; } /** * @psalm-return non-negative-int */ public function linesOfCode(): int { return $this->linesOfCode; } /** * @psalm-return non-negative-int */ public function commentLinesOfCode(): int { return $this->commentLinesOfCode; } /** * @psalm-return non-negative-int */ public function nonCommentLinesOfCode(): int { return $this->nonCommentLinesOfCode; } /** * @psalm-return non-negative-int */ public function logicalLinesOfCode(): int { return $this->logicalLinesOfCode; } public function plus(self $other): self { return new self( $this->linesOfCode() + $other->linesOfCode(), $this->commentLinesOfCode() + $other->commentLinesOfCode(), $this->nonCommentLinesOfCode() + $other->nonCommentLinesOfCode(), $this->logicalLinesOfCode() + $other->logicalLinesOfCode(), ); } } LineCountingVisitor.php 0000644 00000004434 15107320450 0011235 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use function array_merge; use function array_unique; use function assert; use function count; use PhpParser\Comment; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\NodeVisitorAbstract; final class LineCountingVisitor extends NodeVisitorAbstract { /** * @psalm-var non-negative-int */ private readonly int $linesOfCode; /** * @var Comment[] */ private array $comments = []; /** * @var int[] */ private array $linesWithStatements = []; /** * @psalm-param non-negative-int $linesOfCode */ public function __construct(int $linesOfCode) { $this->linesOfCode = $linesOfCode; } public function enterNode(Node $node): void { $this->comments = array_merge($this->comments, $node->getComments()); if (!$node instanceof Expr) { return; } $this->linesWithStatements[] = $node->getStartLine(); } public function result(): LinesOfCode { $commentLinesOfCode = 0; foreach ($this->comments() as $comment) { $commentLinesOfCode += ($comment->getEndLine() - $comment->getStartLine() + 1); } $nonCommentLinesOfCode = $this->linesOfCode - $commentLinesOfCode; $logicalLinesOfCode = count(array_unique($this->linesWithStatements)); assert($commentLinesOfCode >= 0); assert($nonCommentLinesOfCode >= 0); assert($logicalLinesOfCode >= 0); return new LinesOfCode( $this->linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode, $logicalLinesOfCode, ); } /** * @return Comment[] */ private function comments(): array { $comments = []; foreach ($this->comments as $comment) { $comments[$comment->getStartLine() . '_' . $comment->getStartTokenPos() . '_' . $comment->getEndLine() . '_' . $comment->getEndTokenPos()] = $comment; } return $comments; } } Counter.php 0000644 00000004653 15107320450 0006701 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of sebastian/lines-of-code. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\LinesOfCode; use function assert; use function file_get_contents; use function substr_count; use PhpParser\Error; use PhpParser\Lexer; use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\Parser; use PhpParser\ParserFactory; final class Counter { /** * @throws RuntimeException */ public function countInSourceFile(string $sourceFile): LinesOfCode { return $this->countInSourceString(file_get_contents($sourceFile)); } /** * @throws RuntimeException */ public function countInSourceString(string $source): LinesOfCode { $linesOfCode = substr_count($source, "\n"); if ($linesOfCode === 0 && !empty($source)) { $linesOfCode = 1; } assert($linesOfCode >= 0); try { $nodes = $this->parser()->parse($source); assert($nodes !== null); return $this->countInAbstractSyntaxTree($linesOfCode, $nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd } /** * @psalm-param non-negative-int $linesOfCode * * @param Node[] $nodes * * @throws RuntimeException */ public function countInAbstractSyntaxTree(int $linesOfCode, array $nodes): LinesOfCode { $traverser = new NodeTraverser; $visitor = new LineCountingVisitor($linesOfCode); $traverser->addVisitor($visitor); try { /* @noinspection UnusedFunctionResultInspection */ $traverser->traverse($nodes); // @codeCoverageIgnoreStart } catch (Error $error) { throw new RuntimeException( $error->getMessage(), $error->getCode(), $error, ); } // @codeCoverageIgnoreEnd return $visitor->result(); } private function parser(): Parser { return (new ParserFactory)->create(ParserFactory::PREFER_PHP7, new Lexer); } } AbstractLexer.php 0000644 00000016713 15107322110 0010020 0 ustar 00 <?php declare(strict_types=1); namespace Doctrine\Common\Lexer; use ReflectionClass; use UnitEnum; use function get_class; use function implode; use function preg_split; use function sprintf; use function substr; use const PREG_SPLIT_DELIM_CAPTURE; use const PREG_SPLIT_NO_EMPTY; use const PREG_SPLIT_OFFSET_CAPTURE; /** * Base class for writing simple lexers, i.e. for creating small DSLs. * * @template T of UnitEnum|string|int * @template V of string|int */ abstract class AbstractLexer { /** * Lexer original input string. * * @var string */ private $input; /** * Array of scanned tokens. * * @var list<Token<T, V>> */ private $tokens = []; /** * Current lexer position in input string. * * @var int */ private $position = 0; /** * Current peek of current lexer position. * * @var int */ private $peek = 0; /** * The next token in the input. * * @var mixed[]|null * @psalm-var Token<T, V>|null */ public $lookahead; /** * The last matched/seen token. * * @var mixed[]|null * @psalm-var Token<T, V>|null */ public $token; /** * Composed regex for input parsing. * * @var string|null */ private $regex; /** * Sets the input data to be tokenized. * * The Lexer is immediately reset and the new input tokenized. * Any unprocessed tokens from any previous input are lost. * * @param string $input The input to be tokenized. * * @return void */ public function setInput($input) { $this->input = $input; $this->tokens = []; $this->reset(); $this->scan($input); } /** * Resets the lexer. * * @return void */ public function reset() { $this->lookahead = null; $this->token = null; $this->peek = 0; $this->position = 0; } /** * Resets the peek pointer to 0. * * @return void */ public function resetPeek() { $this->peek = 0; } /** * Resets the lexer position on the input to the given position. * * @param int $position Position to place the lexical scanner. * * @return void */ public function resetPosition($position = 0) { $this->position = $position; } /** * Retrieve the original lexer's input until a given position. * * @param int $position * * @return string */ public function getInputUntilPosition($position) { return substr($this->input, 0, $position); } /** * Checks whether a given token matches the current lookahead. * * @param T $type * * @return bool * * @psalm-assert-if-true !=null $this->lookahead */ public function isNextToken($type) { return $this->lookahead !== null && $this->lookahead->isA($type); } /** * Checks whether any of the given tokens matches the current lookahead. * * @param list<T> $types * * @return bool * * @psalm-assert-if-true !=null $this->lookahead */ public function isNextTokenAny(array $types) { return $this->lookahead !== null && $this->lookahead->isA(...$types); } /** * Moves to the next token in the input string. * * @return bool * * @psalm-assert-if-true !null $this->lookahead */ public function moveNext() { $this->peek = 0; $this->token = $this->lookahead; $this->lookahead = isset($this->tokens[$this->position]) ? $this->tokens[$this->position++] : null; return $this->lookahead !== null; } /** * Tells the lexer to skip input tokens until it sees a token with the given value. * * @param T $type The token type to skip until. * * @return void */ public function skipUntil($type) { while ($this->lookahead !== null && ! $this->lookahead->isA($type)) { $this->moveNext(); } } /** * Checks if given value is identical to the given token. * * @param string $value * @param int|string $token * * @return bool */ public function isA($value, $token) { return $this->getType($value) === $token; } /** * Moves the lookahead token forward. * * @return mixed[]|null The next token or NULL if there are no more tokens ahead. * @psalm-return Token<T, V>|null */ public function peek() { if (isset($this->tokens[$this->position + $this->peek])) { return $this->tokens[$this->position + $this->peek++]; } return null; } /** * Peeks at the next token, returns it and immediately resets the peek. * * @return mixed[]|null The next token or NULL if there are no more tokens ahead. * @psalm-return Token<T, V>|null */ public function glimpse() { $peek = $this->peek(); $this->peek = 0; return $peek; } /** * Scans the input string for tokens. * * @param string $input A query string. * * @return void */ protected function scan($input) { if (! isset($this->regex)) { $this->regex = sprintf( '/(%s)|%s/%s', implode(')|(', $this->getCatchablePatterns()), implode('|', $this->getNonCatchablePatterns()), $this->getModifiers() ); } $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; $matches = preg_split($this->regex, $input, -1, $flags); if ($matches === false) { // Work around https://bugs.php.net/78122 $matches = [[$input, 0]]; } foreach ($matches as $match) { // Must remain before 'value' assignment since it can change content $firstMatch = $match[0]; $type = $this->getType($firstMatch); $this->tokens[] = new Token( $firstMatch, $type, $match[1] ); } } /** * Gets the literal for a given token. * * @param T $token * * @return int|string */ public function getLiteral($token) { if ($token instanceof UnitEnum) { return get_class($token) . '::' . $token->name; } $className = static::class; $reflClass = new ReflectionClass($className); $constants = $reflClass->getConstants(); foreach ($constants as $name => $value) { if ($value === $token) { return $className . '::' . $name; } } return $token; } /** * Regex modifiers * * @return string */ protected function getModifiers() { return 'iu'; } /** * Lexical catchable patterns. * * @return string[] */ abstract protected function getCatchablePatterns(); /** * Lexical non-catchable patterns. * * @return string[] */ abstract protected function getNonCatchablePatterns(); /** * Retrieve token type. Also processes the token value if necessary. * * @param string $value * * @return T|null * * @param-out V $value */ abstract protected function getType(&$value); } Token.php 0000644 00000006361 15107322110 0006333 0 ustar 00 <?php declare(strict_types=1); namespace Doctrine\Common\Lexer; use ArrayAccess; use Doctrine\Deprecations\Deprecation; use ReturnTypeWillChange; use UnitEnum; use function in_array; /** * @template T of UnitEnum|string|int * @template V of string|int * @implements ArrayAccess<string,mixed> */ final class Token implements ArrayAccess { /** * The string value of the token in the input string * * @readonly * @var V */ public $value; /** * The type of the token (identifier, numeric, string, input parameter, none) * * @readonly * @var T|null */ public $type; /** * The position of the token in the input string * * @readonly * @var int */ public $position; /** * @param V $value * @param T|null $type */ public function __construct($value, $type, int $position) { $this->value = $value; $this->type = $type; $this->position = $position; } /** @param T ...$types */ public function isA(...$types): bool { return in_array($this->type, $types, true); } /** * @deprecated Use the value, type or position property instead * {@inheritDoc} */ public function offsetExists($offset): bool { Deprecation::trigger( 'doctrine/lexer', 'https://github.com/doctrine/lexer/pull/79', 'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead', self::class ); return in_array($offset, ['value', 'type', 'position'], true); } /** * @deprecated Use the value, type or position property instead * {@inheritDoc} * * @param O $offset * * @return mixed * @psalm-return ( * O is 'value' * ? V * : ( * O is 'type' * ? T|null * : ( * O is 'position' * ? int * : mixed * ) * ) * ) * * @template O of array-key */ #[ReturnTypeWillChange] public function offsetGet($offset) { Deprecation::trigger( 'doctrine/lexer', 'https://github.com/doctrine/lexer/pull/79', 'Accessing %s properties via ArrayAccess is deprecated, use the value, type or position property instead', self::class ); return $this->$offset; } /** * @deprecated no replacement planned * {@inheritDoc} */ public function offsetSet($offset, $value): void { Deprecation::trigger( 'doctrine/lexer', 'https://github.com/doctrine/lexer/pull/79', 'Setting %s properties via ArrayAccess is deprecated', self::class ); $this->$offset = $value; } /** * @deprecated no replacement planned * {@inheritDoc} */ public function offsetUnset($offset): void { Deprecation::trigger( 'doctrine/lexer', 'https://github.com/doctrine/lexer/pull/79', 'Setting %s properties via ArrayAccess is deprecated', self::class ); $this->$offset = null; } } Proxy/ProxyGenerator.php 0000644 00000115634 15107332726 0011406 0 ustar 00 <?php namespace Doctrine\Common\Proxy; use BackedEnum; use Doctrine\Common\Proxy\Exception\InvalidArgumentException; use Doctrine\Common\Proxy\Exception\UnexpectedValueException; use Doctrine\Common\Util\ClassUtils; use Doctrine\Persistence\Mapping\ClassMetadata; use ReflectionIntersectionType; use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; use ReflectionType; use ReflectionUnionType; use function array_combine; use function array_diff; use function array_key_exists; use function array_map; use function array_slice; use function array_unique; use function assert; use function bin2hex; use function call_user_func; use function chmod; use function class_exists; use function dirname; use function explode; use function file; use function file_put_contents; use function get_class; use function implode; use function in_array; use function interface_exists; use function is_callable; use function is_dir; use function is_scalar; use function is_string; use function is_writable; use function lcfirst; use function ltrim; use function method_exists; use function mkdir; use function preg_match; use function preg_match_all; use function preg_replace; use function preg_split; use function random_bytes; use function rename; use function rtrim; use function sprintf; use function str_replace; use function strpos; use function strrev; use function strtolower; use function strtr; use function substr; use function trim; use function var_export; use const DIRECTORY_SEPARATOR; use const PHP_VERSION_ID; use const PREG_SPLIT_DELIM_CAPTURE; /** * This factory is used to generate proxy classes. * It builds proxies from given parameters, a template and class metadata. */ class ProxyGenerator { /** * Used to match very simple id methods that don't need * to be decorated since the identifier is known. */ public const PATTERN_MATCH_ID_METHOD = <<<'EOT' ((?(DEFINE) (?<type>\\?[a-z_\x7f-\xff][\w\x7f-\xff]*(?:\\[a-z_\x7f-\xff][\w\x7f-\xff]*)*) (?<intersection_type>(?&type)\s*&\s*(?&type)) (?<union_type>(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type))(?:\s*\|\s*(?:(?:\(\s*(?&intersection_type)\s*\))|(?&type)))+) )(?:public\s+)?(?:function\s+%s\s*\(\)\s*)\s*(?::\s*(?:(?&union_type)|(?&intersection_type)|(?:\??(?&type)))\s*)?{\s*return\s*\$this->%s;\s*})i EOT; /** * The namespace that contains all proxy classes. * * @var string */ private $proxyNamespace; /** * The directory that contains all proxy classes. * * @var string */ private $proxyDirectory; /** * Map of callables used to fill in placeholders set in the template. * * @var string[]|callable[] */ protected $placeholders = [ 'baseProxyInterface' => Proxy::class, 'additionalProperties' => '', ]; /** * Template used as a blueprint to generate proxies. * * @var string */ protected $proxyClassTemplate = '<?php namespace <namespace>; <enumUseStatements> /** * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE\'S PROXY GENERATOR */ class <proxyShortClassName> extends \<className> implements \<baseProxyInterface> { /** * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with * three parameters, being respectively the proxy object to be initialized, the method that triggered the * initialization process and an array of ordered parameters that were passed to that method. * * @see \Doctrine\Common\Proxy\Proxy::__setInitializer */ public $__initializer__; /** * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object * * @see \Doctrine\Common\Proxy\Proxy::__setCloner */ public $__cloner__; /** * @var boolean flag indicating if this object was already initialized * * @see \Doctrine\Persistence\Proxy::__isInitialized */ public $__isInitialized__ = false; /** * @var array<string, null> properties to be lazy loaded, indexed by property name */ public static $lazyPropertiesNames = <lazyPropertiesNames>; /** * @var array<string, mixed> default values of properties to be lazy loaded, with keys being the property names * * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties */ public static $lazyPropertiesDefaults = <lazyPropertiesDefaults>; <additionalProperties> <constructorImpl> <magicGet> <magicSet> <magicIsset> <sleepImpl> <wakeupImpl> <cloneImpl> /** * Forces initialization of the proxy */ public function __load(): void { $this->__initializer__ && $this->__initializer__->__invoke($this, \'__load\', []); } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ public function __isInitialized(): bool { return $this->__isInitialized__; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ public function __setInitialized($initialized): void { $this->__isInitialized__ = $initialized; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ public function __setInitializer(\Closure $initializer = null): void { $this->__initializer__ = $initializer; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ public function __getInitializer(): ?\Closure { return $this->__initializer__; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ public function __setCloner(\Closure $cloner = null): void { $this->__cloner__ = $cloner; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific cloning logic */ public function __getCloner(): ?\Closure { return $this->__cloner__; } /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic * @deprecated no longer in use - generated code now relies on internal components rather than generated public API * @static */ public function __getLazyProperties(): array { return self::$lazyPropertiesDefaults; } <methods> } '; /** * Initializes a new instance of the <tt>ProxyFactory</tt> class that is * connected to the given <tt>EntityManager</tt>. * * @param string $proxyDirectory The directory to use for the proxy classes. It must exist. * @param string $proxyNamespace The namespace to use for the proxy classes. * * @throws InvalidArgumentException */ public function __construct($proxyDirectory, $proxyNamespace) { if (! $proxyDirectory) { throw InvalidArgumentException::proxyDirectoryRequired(); } if (! $proxyNamespace) { throw InvalidArgumentException::proxyNamespaceRequired(); } $this->proxyDirectory = $proxyDirectory; $this->proxyNamespace = $proxyNamespace; } /** * Sets a placeholder to be replaced in the template. * * @param string $name * @param string|callable $placeholder * * @throws InvalidArgumentException */ public function setPlaceholder($name, $placeholder) { if (! is_string($placeholder) && ! is_callable($placeholder)) { throw InvalidArgumentException::invalidPlaceholder($name); } $this->placeholders[$name] = $placeholder; } /** * Sets the base template used to create proxy classes. * * @param string $proxyClassTemplate */ public function setProxyClassTemplate($proxyClassTemplate) { $this->proxyClassTemplate = (string) $proxyClassTemplate; } /** * Generates a proxy class file. * * @param ClassMetadata $class Metadata for the original class. * @param string|bool $fileName Filename (full path) for the generated class. If none is given, eval() is used. * * @throws InvalidArgumentException * @throws UnexpectedValueException */ public function generateProxyClass(ClassMetadata $class, $fileName = false) { $this->verifyClassCanBeProxied($class); preg_match_all('(<([a-zA-Z]+)>)', $this->proxyClassTemplate, $placeholderMatches); $placeholderMatches = array_combine($placeholderMatches[0], $placeholderMatches[1]); $placeholders = []; foreach ($placeholderMatches as $placeholder => $name) { $placeholders[$placeholder] = $this->placeholders[$name] ?? [$this, 'generate' . $name]; } foreach ($placeholders as & $placeholder) { if (! is_callable($placeholder)) { continue; } $placeholder = call_user_func($placeholder, $class); } $proxyCode = strtr($this->proxyClassTemplate, $placeholders); if (! $fileName) { $proxyClassName = $this->generateNamespace($class) . '\\' . $this->generateProxyShortClassName($class); if (! class_exists($proxyClassName)) { eval(substr($proxyCode, 5)); } return; } $parentDirectory = dirname($fileName); if (! is_dir($parentDirectory) && (@mkdir($parentDirectory, 0775, true) === false)) { throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory); } if (! is_writable($parentDirectory)) { throw UnexpectedValueException::proxyDirectoryNotWritable($this->proxyDirectory); } $tmpFileName = $fileName . '.' . bin2hex(random_bytes(12)); file_put_contents($tmpFileName, $proxyCode); @chmod($tmpFileName, 0664); rename($tmpFileName, $fileName); } /** @throws InvalidArgumentException */ private function verifyClassCanBeProxied(ClassMetadata $class) { if ($class->getReflectionClass()->isFinal()) { throw InvalidArgumentException::classMustNotBeFinal($class->getName()); } if ($class->getReflectionClass()->isAbstract()) { throw InvalidArgumentException::classMustNotBeAbstract($class->getName()); } if (PHP_VERSION_ID >= 80200 && $class->getReflectionClass()->isReadOnly()) { throw InvalidArgumentException::classMustNotBeReadOnly($class->getName()); } } /** * Generates the proxy short class name to be used in the template. * * @return string */ private function generateProxyShortClassName(ClassMetadata $class) { $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace); $parts = explode('\\', strrev($proxyClassName), 2); return strrev($parts[0]); } /** * Generates the proxy namespace. * * @return string */ private function generateNamespace(ClassMetadata $class) { $proxyClassName = ClassUtils::generateProxyClassName($class->getName(), $this->proxyNamespace); $parts = explode('\\', strrev($proxyClassName), 2); return strrev($parts[1]); } /** * Enums must have a use statement when used as public property defaults. */ public function generateEnumUseStatements(ClassMetadata $class): string { if (PHP_VERSION_ID < 80100) { return "\n"; } $defaultProperties = $class->getReflectionClass()->getDefaultProperties(); $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $enumClasses = []; foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); if (! in_array($name, $lazyLoadedPublicProperties, true)) { continue; } if (array_key_exists($name, $defaultProperties) && $defaultProperties[$name] instanceof BackedEnum) { $enumClassNameParts = explode('\\', get_class($defaultProperties[$name])); $enumClasses[] = $enumClassNameParts[0]; } } return implode( "\n", array_map( static function ($className) { return 'use ' . $className . ';'; }, array_unique($enumClasses) ) ) . "\n"; } /** * Generates the original class name. * * @return string */ private function generateClassName(ClassMetadata $class) { return ltrim($class->getName(), '\\'); } /** * Generates the array representation of lazy loaded public properties and their default values. * * @return string */ private function generateLazyPropertiesNames(ClassMetadata $class) { $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $values = []; foreach ($lazyPublicProperties as $name) { $values[$name] = null; } return var_export($values, true); } /** * Generates the array representation of lazy loaded public properties names. * * @return string */ private function generateLazyPropertiesDefaults(ClassMetadata $class) { return var_export($this->getLazyLoadedPublicProperties($class), true); } /** * Generates the constructor code (un-setting public lazy loaded properties, setting identifier field values). * * @return string */ private function generateConstructorImpl(ClassMetadata $class) { $constructorImpl = <<<'EOT' public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) { EOT; $toUnset = array_map(static function (string $name): string { return '$this->' . $name; }, $this->getLazyLoadedPublicPropertiesNames($class)); return $constructorImpl . ($toUnset === [] ? '' : ' unset(' . implode(', ', $toUnset) . ");\n") . <<<'EOT' $this->__initializer__ = $initializer; $this->__cloner__ = $cloner; } EOT; } /** * Generates the magic getter invoked when lazy loaded public properties are requested. * * @return string */ private function generateMagicGet(ClassMetadata $class) { $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $reflectionClass = $class->getReflectionClass(); $hasParentGet = false; $returnReference = ''; $inheritDoc = ''; $name = '$name'; $parametersString = '$name'; $returnTypeHint = null; if ($reflectionClass->hasMethod('__get')) { $hasParentGet = true; $inheritDoc = '{@inheritDoc}'; $methodReflection = $reflectionClass->getMethod('__get'); if ($methodReflection->returnsReference()) { $returnReference = '& '; } $methodParameters = $methodReflection->getParameters(); $name = '$' . $methodParameters[0]->getName(); $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']); $returnTypeHint = $this->getMethodReturnType($methodReflection); } if (empty($lazyPublicProperties) && ! $hasParentGet) { return ''; } $magicGet = <<<EOT /** * $inheritDoc * @param string \$name */ public function {$returnReference}__get($parametersString)$returnTypeHint { EOT; if (! empty($lazyPublicProperties)) { $magicGet .= <<<'EOT' if (\array_key_exists($name, self::$lazyPropertiesNames)) { $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]); EOT; if ($returnTypeHint === ': void') { $magicGet .= "\n return;"; } else { $magicGet .= "\n return \$this->\$name;"; } $magicGet .= <<<'EOT' } EOT; } if ($hasParentGet) { $magicGet .= <<<'EOT' $this->__initializer__ && $this->__initializer__->__invoke($this, '__get', [$name]); EOT; if ($returnTypeHint === ': void') { $magicGet .= <<<'EOT' parent::__get($name); return; EOT; } elseif ($returnTypeHint === ': never') { $magicGet .= <<<'EOT' parent::__get($name); EOT; } else { $magicGet .= <<<'EOT' return parent::__get($name); EOT; } } else { $magicGet .= sprintf(<<<EOT trigger_error(sprintf('Undefined property: %%s::$%%s', __CLASS__, %s), E_USER_NOTICE); EOT , $name); } return $magicGet . "\n }"; } /** * Generates the magic setter (currently unused). * * @return string */ private function generateMagicSet(ClassMetadata $class) { $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $reflectionClass = $class->getReflectionClass(); $hasParentSet = false; $inheritDoc = ''; $parametersString = '$name, $value'; $returnTypeHint = null; if ($reflectionClass->hasMethod('__set')) { $hasParentSet = true; $inheritDoc = '{@inheritDoc}'; $methodReflection = $reflectionClass->getMethod('__set'); $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name', 'value']); $returnTypeHint = $this->getMethodReturnType($methodReflection); } if (empty($lazyPublicProperties) && ! $hasParentSet) { return ''; } $magicSet = <<<EOT /** * $inheritDoc * @param string \$name * @param mixed \$value */ public function __set($parametersString)$returnTypeHint { EOT; if (! empty($lazyPublicProperties)) { $magicSet .= <<<'EOT' if (\array_key_exists($name, self::$lazyPropertiesNames)) { $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]); $this->$name = $value; return; } EOT; } if ($hasParentSet) { $magicSet .= <<<'EOT' $this->__initializer__ && $this->__initializer__->__invoke($this, '__set', [$name, $value]); EOT; if ($returnTypeHint === ': void') { $magicSet .= <<<'EOT' parent::__set($name, $value); return; EOT; } elseif ($returnTypeHint === ': never') { $magicSet .= <<<'EOT' parent::__set($name, $value); EOT; } else { $magicSet .= <<<'EOT' return parent::__set($name, $value); EOT; } } else { $magicSet .= ' $this->$name = $value;'; } return $magicSet . "\n }"; } /** * Generates the magic issetter invoked when lazy loaded public properties are checked against isset(). * * @return string */ private function generateMagicIsset(ClassMetadata $class) { $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $hasParentIsset = $class->getReflectionClass()->hasMethod('__isset'); $parametersString = '$name'; $returnTypeHint = null; if ($hasParentIsset) { $methodReflection = $class->getReflectionClass()->getMethod('__isset'); $parametersString = $this->buildParametersString($methodReflection->getParameters(), ['name']); $returnTypeHint = $this->getMethodReturnType($methodReflection); } if (empty($lazyPublicProperties) && ! $hasParentIsset) { return ''; } $inheritDoc = $hasParentIsset ? '{@inheritDoc}' : ''; $magicIsset = <<<EOT /** * $inheritDoc * @param string \$name * @return boolean */ public function __isset($parametersString)$returnTypeHint { EOT; if (! empty($lazyPublicProperties)) { $magicIsset .= <<<'EOT' if (\array_key_exists($name, self::$lazyPropertiesNames)) { $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]); return isset($this->$name); } EOT; } if ($hasParentIsset) { $magicIsset .= <<<'EOT' $this->__initializer__ && $this->__initializer__->__invoke($this, '__isset', [$name]); return parent::__isset($name); EOT; } else { $magicIsset .= ' return false;'; } return $magicIsset . "\n }"; } /** * Generates implementation for the `__sleep` method of proxies. * * @return string */ private function generateSleepImpl(ClassMetadata $class) { $reflectionClass = $class->getReflectionClass(); $hasParentSleep = $reflectionClass->hasMethod('__sleep'); $inheritDoc = $hasParentSleep ? '{@inheritDoc}' : ''; $returnTypeHint = $hasParentSleep ? $this->getMethodReturnType($reflectionClass->getMethod('__sleep')) : ''; $sleepImpl = <<<EOT /** * $inheritDoc * @return array */ public function __sleep()$returnTypeHint { EOT; if ($hasParentSleep) { return $sleepImpl . <<<'EOT' $properties = array_merge(['__isInitialized__'], parent::__sleep()); if ($this->__isInitialized__) { $properties = array_diff($properties, array_keys(self::$lazyPropertiesNames)); } return $properties; } EOT; } $allProperties = ['__isInitialized__']; foreach ($class->getReflectionClass()->getProperties() as $prop) { assert($prop instanceof ReflectionProperty); if ($prop->isStatic()) { continue; } $allProperties[] = $prop->isPrivate() ? "\0" . $prop->getDeclaringClass()->getName() . "\0" . $prop->getName() : $prop->getName(); } $lazyPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $protectedProperties = array_diff($allProperties, $lazyPublicProperties); foreach ($allProperties as &$property) { $property = var_export($property, true); } foreach ($protectedProperties as &$property) { $property = var_export($property, true); } $allProperties = implode(', ', $allProperties); $protectedProperties = implode(', ', $protectedProperties); return $sleepImpl . <<<EOT if (\$this->__isInitialized__) { return [$allProperties]; } return [$protectedProperties]; } EOT; } /** * Generates implementation for the `__wakeup` method of proxies. * * @return string */ private function generateWakeupImpl(ClassMetadata $class) { $reflectionClass = $class->getReflectionClass(); $hasParentWakeup = $reflectionClass->hasMethod('__wakeup'); $unsetPublicProperties = []; foreach ($this->getLazyLoadedPublicPropertiesNames($class) as $lazyPublicProperty) { $unsetPublicProperties[] = '$this->' . $lazyPublicProperty; } $shortName = $this->generateProxyShortClassName($class); $inheritDoc = $hasParentWakeup ? '{@inheritDoc}' : ''; $returnTypeHint = $hasParentWakeup ? $this->getMethodReturnType($reflectionClass->getMethod('__wakeup')) : ''; $wakeupImpl = <<<EOT /** * $inheritDoc */ public function __wakeup()$returnTypeHint { if ( ! \$this->__isInitialized__) { \$this->__initializer__ = function ($shortName \$proxy) { \$proxy->__setInitializer(null); \$proxy->__setCloner(null); \$existingProperties = get_object_vars(\$proxy); foreach (\$proxy::\$lazyPropertiesDefaults as \$property => \$defaultValue) { if ( ! array_key_exists(\$property, \$existingProperties)) { \$proxy->\$property = \$defaultValue; } } }; EOT; if (! empty($unsetPublicProperties)) { $wakeupImpl .= "\n unset(" . implode(', ', $unsetPublicProperties) . ');'; } $wakeupImpl .= "\n }"; if ($hasParentWakeup) { $wakeupImpl .= "\n parent::__wakeup();"; } $wakeupImpl .= "\n }"; return $wakeupImpl; } /** * Generates implementation for the `__clone` method of proxies. * * @return string */ private function generateCloneImpl(ClassMetadata $class) { $reflectionClass = $class->getReflectionClass(); $hasParentClone = $reflectionClass->hasMethod('__clone'); $returnTypeHint = $hasParentClone ? $this->getMethodReturnType($reflectionClass->getMethod('__clone')) : ''; $inheritDoc = $hasParentClone ? '{@inheritDoc}' : ''; $callParentClone = $hasParentClone ? "\n parent::__clone();\n" : ''; return <<<EOT /** * $inheritDoc */ public function __clone()$returnTypeHint { \$this->__cloner__ && \$this->__cloner__->__invoke(\$this, '__clone', []); $callParentClone } EOT; } /** * Generates decorated methods by picking those available in the parent class. * * @return string */ private function generateMethods(ClassMetadata $class) { $methods = ''; $methodNames = []; $reflectionMethods = $class->getReflectionClass()->getMethods(ReflectionMethod::IS_PUBLIC); $skippedMethods = [ '__sleep' => true, '__clone' => true, '__wakeup' => true, '__get' => true, '__set' => true, '__isset' => true, ]; foreach ($reflectionMethods as $method) { $name = $method->getName(); if ( $method->isConstructor() || isset($skippedMethods[strtolower($name)]) || isset($methodNames[$name]) || $method->isFinal() || $method->isStatic() || ( ! $method->isPublic()) ) { continue; } $methodNames[$name] = true; $methods .= "\n /**\n" . " * {@inheritDoc}\n" . " */\n" . ' public function '; if ($method->returnsReference()) { $methods .= '&'; } $methods .= $name . '(' . $this->buildParametersString($method->getParameters()) . ')'; $methods .= $this->getMethodReturnType($method); $methods .= "\n" . ' {' . "\n"; if ($this->isShortIdentifierGetter($method, $class)) { $identifier = lcfirst(substr($name, 3)); $fieldType = $class->getTypeOfField($identifier); $cast = in_array($fieldType, ['integer', 'smallint']) ? '(int) ' : ''; $methods .= ' if ($this->__isInitialized__ === false) {' . "\n"; $methods .= ' '; $methods .= $this->shouldProxiedMethodReturn($method) ? 'return ' : ''; $methods .= $cast . ' parent::' . $method->getName() . "();\n"; $methods .= ' }' . "\n\n"; } $invokeParamsString = implode(', ', $this->getParameterNamesForInvoke($method->getParameters())); $callParamsString = implode(', ', $this->getParameterNamesForParentCall($method->getParameters())); $methods .= "\n \$this->__initializer__ " . '&& $this->__initializer__->__invoke($this, ' . var_export($name, true) . ', [' . $invokeParamsString . ']);' . "\n\n " . ($this->shouldProxiedMethodReturn($method) ? 'return ' : '') . 'parent::' . $name . '(' . $callParamsString . ');' . "\n" . ' }' . "\n"; } return $methods; } /** * Generates the Proxy file name. * * @param string $className * @param string $baseDirectory Optional base directory for proxy file name generation. * If not specified, the directory configured on the Configuration of the * EntityManager will be used by this factory. * @psalm-param class-string $className * * @return string */ public function getProxyFileName($className, $baseDirectory = null) { $baseDirectory = $baseDirectory ?: $this->proxyDirectory; return rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . Proxy::MARKER . str_replace('\\', '', $className) . '.php'; } /** * Checks if the method is a short identifier getter. * * What does this mean? For proxy objects the identifier is already known, * however accessing the getter for this identifier usually triggers the * lazy loading, leading to a query that may not be necessary if only the * ID is interesting for the userland code (for example in views that * generate links to the entity, but do not display anything else). * * @param ReflectionMethod $method * * @return bool */ private function isShortIdentifierGetter($method, ClassMetadata $class) { $identifier = lcfirst(substr($method->getName(), 3)); $startLine = $method->getStartLine(); $endLine = $method->getEndLine(); $cheapCheck = $method->getNumberOfParameters() === 0 && substr($method->getName(), 0, 3) === 'get' && in_array($identifier, $class->getIdentifier(), true) && $class->hasField($identifier) && ($endLine - $startLine <= 4); if ($cheapCheck) { $code = file($method->getFileName()); $code = trim(implode(' ', array_slice($code, $startLine - 1, $endLine - $startLine + 1))); $pattern = sprintf(self::PATTERN_MATCH_ID_METHOD, $method->getName(), $identifier); if (preg_match($pattern, $code)) { return true; } } return false; } /** * Generates the list of public properties to be lazy loaded. * * @return array<int, string> */ private function getLazyLoadedPublicPropertiesNames(ClassMetadata $class): array { $properties = []; foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); if ((! $class->hasField($name) && ! $class->hasAssociation($name)) || $class->isIdentifier($name)) { continue; } $properties[] = $name; } return $properties; } /** * Generates the list of default values of public properties. * * @return mixed[] */ private function getLazyLoadedPublicProperties(ClassMetadata $class) { $defaultProperties = $class->getReflectionClass()->getDefaultProperties(); $lazyLoadedPublicProperties = $this->getLazyLoadedPublicPropertiesNames($class); $defaultValues = []; foreach ($class->getReflectionClass()->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); if (! in_array($name, $lazyLoadedPublicProperties, true)) { continue; } if (array_key_exists($name, $defaultProperties)) { $defaultValues[$name] = $defaultProperties[$name]; } elseif (method_exists($property, 'getType')) { $propertyType = $property->getType(); if ($propertyType !== null && $propertyType->allowsNull()) { $defaultValues[$name] = null; } } } return $defaultValues; } /** * @param ReflectionParameter[] $parameters * @param string[] $renameParameters * * @return string */ private function buildParametersString(array $parameters, array $renameParameters = []) { $parameterDefinitions = []; $i = -1; foreach ($parameters as $param) { assert($param instanceof ReflectionParameter); $i++; $parameterDefinition = ''; $parameterType = $this->getParameterType($param); if ($parameterType !== null) { $parameterDefinition .= $parameterType . ' '; } if ($param->isPassedByReference()) { $parameterDefinition .= '&'; } if ($param->isVariadic()) { $parameterDefinition .= '...'; } $parameterDefinition .= '$' . ($renameParameters ? $renameParameters[$i] : $param->getName()); $parameterDefinition .= $this->getParameterDefaultValue($param); $parameterDefinitions[] = $parameterDefinition; } return implode(', ', $parameterDefinitions); } /** @return string|null */ private function getParameterType(ReflectionParameter $parameter) { if (! $parameter->hasType()) { return null; } $declaringFunction = $parameter->getDeclaringFunction(); assert($declaringFunction instanceof ReflectionMethod); return $this->formatType($parameter->getType(), $declaringFunction, $parameter); } /** @return string */ private function getParameterDefaultValue(ReflectionParameter $parameter) { if (! $parameter->isDefaultValueAvailable()) { return ''; } if (PHP_VERSION_ID < 80100 || is_scalar($parameter->getDefaultValue())) { return ' = ' . var_export($parameter->getDefaultValue(), true); } $value = rtrim(substr(explode('$' . $parameter->getName() . ' = ', (string) $parameter, 2)[1], 0, -2)); if (strpos($value, '\\') !== false || strpos($value, '::') !== false) { $value = preg_split("/('(?:[^'\\\\]*+(?:\\\\.)*+)*+')/", $value, -1, PREG_SPLIT_DELIM_CAPTURE); foreach ($value as $i => $part) { if ($i % 2 === 0) { $value[$i] = preg_replace('/(?<![a-zA-Z0-9_\x7f-\xff\\\\])[a-zA-Z0-9_\x7f-\xff]++(?:\\\\[a-zA-Z0-9_\x7f-\xff]++|::)++/', '\\\\\0', $part); } } $value = implode('', $value); } return ' = ' . $value; } /** * @param ReflectionParameter[] $parameters * * @return string[] */ private function getParameterNamesForInvoke(array $parameters) { return array_map( static function (ReflectionParameter $parameter) { return '$' . $parameter->getName(); }, $parameters ); } /** * @param ReflectionParameter[] $parameters * * @return string[] */ private function getParameterNamesForParentCall(array $parameters) { return array_map( static function (ReflectionParameter $parameter) { $name = ''; if ($parameter->isVariadic()) { $name .= '...'; } $name .= '$' . $parameter->getName(); return $name; }, $parameters ); } /** @return string */ private function getMethodReturnType(ReflectionMethod $method) { if (! $method->hasReturnType()) { return ''; } return ': ' . $this->formatType($method->getReturnType(), $method); } /** @return bool */ private function shouldProxiedMethodReturn(ReflectionMethod $method) { if (! $method->hasReturnType()) { return true; } return ! in_array( strtolower($this->formatType($method->getReturnType(), $method)), ['void', 'never'], true ); } /** @return string */ private function formatType( ReflectionType $type, ReflectionMethod $method, ?ReflectionParameter $parameter = null ) { if ($type instanceof ReflectionUnionType) { return implode('|', array_map( function (ReflectionType $unionedType) use ($method, $parameter) { if ($unionedType instanceof ReflectionIntersectionType) { return '(' . $this->formatType($unionedType, $method, $parameter) . ')'; } return $this->formatType($unionedType, $method, $parameter); }, $type->getTypes() )); } if ($type instanceof ReflectionIntersectionType) { return implode('&', array_map( function (ReflectionType $intersectedType) use ($method, $parameter) { return $this->formatType($intersectedType, $method, $parameter); }, $type->getTypes() )); } assert($type instanceof ReflectionNamedType); $name = $type->getName(); $nameLower = strtolower($name); if ($nameLower === 'static') { $name = 'static'; } if ($nameLower === 'self') { $name = $method->getDeclaringClass()->getName(); } if ($nameLower === 'parent') { $name = $method->getDeclaringClass()->getParentClass()->getName(); } if (! $type->isBuiltin() && ! class_exists($name) && ! interface_exists($name) && $name !== 'static') { if ($parameter !== null) { throw UnexpectedValueException::invalidParameterTypeHint( $method->getDeclaringClass()->getName(), $method->getName(), $parameter->getName() ); } throw UnexpectedValueException::invalidReturnTypeHint( $method->getDeclaringClass()->getName(), $method->getName() ); } if (! $type->isBuiltin() && $name !== 'static') { $name = '\\' . $name; } if ( $type->allowsNull() && ! in_array($name, ['mixed', 'null'], true) && ($parameter === null || ! $parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== null) ) { $name = '?' . $name; } return $name; } } Proxy/AbstractProxyFactory.php 0000644 00000017274 15107332726 0012554 0 ustar 00 <?php namespace Doctrine\Common\Proxy; use Doctrine\Common\Proxy\Exception\InvalidArgumentException; use Doctrine\Common\Proxy\Exception\OutOfBoundsException; use Doctrine\Common\Util\ClassUtils; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadataFactory; use function class_exists; use function file_exists; use function filemtime; use function in_array; /** * Abstract factory for proxy objects. */ abstract class AbstractProxyFactory { /** * Never autogenerate a proxy and rely that it was generated by some * process before deployment. */ public const AUTOGENERATE_NEVER = 0; /** * Always generates a new proxy in every request. * * This is only sane during development. */ public const AUTOGENERATE_ALWAYS = 1; /** * Autogenerate the proxy class when the proxy file does not exist. * * This strategy causes a file_exists() call whenever any proxy is used the * first time in a request. */ public const AUTOGENERATE_FILE_NOT_EXISTS = 2; /** * Generate the proxy classes using eval(). * * This strategy is only sane for development, and even then it gives me * the creeps a little. */ public const AUTOGENERATE_EVAL = 3; /** * Autogenerate the proxy class when the proxy file does not exist or * when the proxied file changed. * * This strategy causes a file_exists() call whenever any proxy is used the * first time in a request. When the proxied file is changed, the proxy will * be updated. */ public const AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED = 4; private const AUTOGENERATE_MODES = [ self::AUTOGENERATE_NEVER, self::AUTOGENERATE_ALWAYS, self::AUTOGENERATE_FILE_NOT_EXISTS, self::AUTOGENERATE_EVAL, self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED, ]; /** @var ClassMetadataFactory */ private $metadataFactory; /** @var ProxyGenerator the proxy generator responsible for creating the proxy classes/files. */ private $proxyGenerator; /** @var int Whether to automatically (re)generate proxy classes. */ private $autoGenerate; /** @var ProxyDefinition[] */ private $definitions = []; /** * @param bool|int $autoGenerate * * @throws InvalidArgumentException When auto generate mode is not valid. */ public function __construct(ProxyGenerator $proxyGenerator, ClassMetadataFactory $metadataFactory, $autoGenerate) { $this->proxyGenerator = $proxyGenerator; $this->metadataFactory = $metadataFactory; $this->autoGenerate = (int) $autoGenerate; if (! in_array($this->autoGenerate, self::AUTOGENERATE_MODES, true)) { throw InvalidArgumentException::invalidAutoGenerateMode($autoGenerate); } } /** * Gets a reference proxy instance for the entity of the given type and identified by * the given identifier. * * @param string $className * @param array<mixed> $identifier * * @return Proxy * * @throws OutOfBoundsException */ public function getProxy($className, array $identifier) { $definition = $this->definitions[$className] ?? $this->getProxyDefinition($className); $fqcn = $definition->proxyClassName; $proxy = new $fqcn($definition->initializer, $definition->cloner); foreach ($definition->identifierFields as $idField) { if (! isset($identifier[$idField])) { throw OutOfBoundsException::missingPrimaryKeyValue($className, $idField); } $definition->reflectionFields[$idField]->setValue($proxy, $identifier[$idField]); } return $proxy; } /** * Generates proxy classes for all given classes. * * @param ClassMetadata[] $classes The classes (ClassMetadata instances) * for which to generate proxies. * @param string $proxyDir The target directory of the proxy classes. If not specified, the * directory configured on the Configuration of the EntityManager used * by this factory is used. * * @return int Number of generated proxies. */ public function generateProxyClasses(array $classes, $proxyDir = null) { $generated = 0; foreach ($classes as $class) { if ($this->skipClass($class)) { continue; } $proxyFileName = $this->proxyGenerator->getProxyFileName($class->getName(), $proxyDir); $this->proxyGenerator->generateProxyClass($class, $proxyFileName); $generated += 1; } return $generated; } /** * Reset initialization/cloning logic for an un-initialized proxy * * @return Proxy * * @throws InvalidArgumentException */ public function resetUninitializedProxy(Proxy $proxy) { if ($proxy->__isInitialized()) { throw InvalidArgumentException::unitializedProxyExpected($proxy); } $className = ClassUtils::getClass($proxy); $definition = $this->definitions[$className] ?? $this->getProxyDefinition($className); $proxy->__setInitializer($definition->initializer); $proxy->__setCloner($definition->cloner); return $proxy; } /** * Get a proxy definition for the given class name. * * @param string $className * @psalm-param class-string $className * * @return ProxyDefinition */ private function getProxyDefinition($className) { $classMetadata = $this->metadataFactory->getMetadataFor($className); $className = $classMetadata->getName(); // aliases and case sensitivity $this->definitions[$className] = $this->createProxyDefinition($className); $proxyClassName = $this->definitions[$className]->proxyClassName; if (! class_exists($proxyClassName, false)) { $fileName = $this->proxyGenerator->getProxyFileName($className); switch ($this->autoGenerate) { case self::AUTOGENERATE_NEVER: require $fileName; break; case self::AUTOGENERATE_FILE_NOT_EXISTS: if (! file_exists($fileName)) { $this->proxyGenerator->generateProxyClass($classMetadata, $fileName); } require $fileName; break; case self::AUTOGENERATE_ALWAYS: $this->proxyGenerator->generateProxyClass($classMetadata, $fileName); require $fileName; break; case self::AUTOGENERATE_EVAL: $this->proxyGenerator->generateProxyClass($classMetadata, false); break; case self::AUTOGENERATE_FILE_NOT_EXISTS_OR_CHANGED: if (! file_exists($fileName) || filemtime($fileName) < filemtime($classMetadata->getReflectionClass()->getFileName())) { $this->proxyGenerator->generateProxyClass($classMetadata, $fileName); } require $fileName; break; } } return $this->definitions[$className]; } /** * Determine if this class should be skipped during proxy generation. * * @return bool */ abstract protected function skipClass(ClassMetadata $metadata); /** * @param string $className * @psalm-param class-string $className * * @return ProxyDefinition */ abstract protected function createProxyDefinition($className); } Proxy/Exception/ProxyException.php 0000644 00000000261 15107332726 0013341 0 ustar 00 <?php namespace Doctrine\Common\Proxy\Exception; /** * Base exception interface for proxy exceptions. * * @link www.doctrine-project.org */ interface ProxyException { } Proxy/Exception/UnexpectedValueException.php 0000644 00000003324 15107332726 0015324 0 ustar 00 <?php namespace Doctrine\Common\Proxy\Exception; use Throwable; use UnexpectedValueException as BaseUnexpectedValueException; use function sprintf; /** * Proxy Unexpected Value Exception. * * @link www.doctrine-project.org */ class UnexpectedValueException extends BaseUnexpectedValueException implements ProxyException { /** * @param string $proxyDirectory * * @return self */ public static function proxyDirectoryNotWritable($proxyDirectory) { return new self(sprintf('Your proxy directory "%s" must be writable', $proxyDirectory)); } /** * @param string $className * @param string $methodName * @param string $parameterName * @psalm-param class-string $className * * @return self */ public static function invalidParameterTypeHint( $className, $methodName, $parameterName, ?Throwable $previous = null ) { return new self( sprintf( 'The type hint of parameter "%s" in method "%s" in class "%s" is invalid.', $parameterName, $methodName, $className ), 0, $previous ); } /** * @param string $className * @param string $methodName * @psalm-param class-string $className * * @return self */ public static function invalidReturnTypeHint($className, $methodName, ?Throwable $previous = null) { return new self( sprintf( 'The return type of method "%s" in class "%s" is invalid.', $methodName, $className ), 0, $previous ); } } Proxy/Exception/OutOfBoundsException.php 0000644 00000001172 15107332726 0014431 0 ustar 00 <?php namespace Doctrine\Common\Proxy\Exception; use OutOfBoundsException as BaseOutOfBoundsException; use function sprintf; /** * Proxy Invalid Argument Exception. * * @link www.doctrine-project.org */ class OutOfBoundsException extends BaseOutOfBoundsException implements ProxyException { /** * @param string $className * @param string $idField * @psalm-param class-string $className * * @return self */ public static function missingPrimaryKeyValue($className, $idField) { return new self(sprintf('Missing value for primary key %s on %s', $idField, $className)); } } Proxy/Exception/InvalidArgumentException.php 0000644 00000005662 15107332726 0015323 0 ustar 00 <?php namespace Doctrine\Common\Proxy\Exception; use Doctrine\Persistence\Proxy; use InvalidArgumentException as BaseInvalidArgumentException; use function get_class; use function gettype; use function is_object; use function sprintf; /** * Proxy Invalid Argument Exception. * * @link www.doctrine-project.org */ class InvalidArgumentException extends BaseInvalidArgumentException implements ProxyException { /** @return self */ public static function proxyDirectoryRequired() { return new self('You must configure a proxy directory. See docs for details'); } /** * @param string $className * @param string $proxyNamespace * @psalm-param class-string $className * * @return self */ public static function notProxyClass($className, $proxyNamespace) { return new self(sprintf('The class "%s" is not part of the proxy namespace "%s"', $className, $proxyNamespace)); } /** * @param string $name * * @return self */ public static function invalidPlaceholder($name) { return new self(sprintf('Provided placeholder for "%s" must be either a string or a valid callable', $name)); } /** @return self */ public static function proxyNamespaceRequired() { return new self('You must configure a proxy namespace'); } /** @return self */ public static function unitializedProxyExpected(Proxy $proxy) { return new self(sprintf('Provided proxy of type "%s" must not be initialized.', get_class($proxy))); } /** * @param mixed $callback * * @return self */ public static function invalidClassNotFoundCallback($callback) { $type = is_object($callback) ? get_class($callback) : gettype($callback); return new self(sprintf('Invalid \$notFoundCallback given: must be a callable, "%s" given', $type)); } /** * @param string $className * @psalm-param class-string $className * * @return self */ public static function classMustNotBeAbstract($className) { return new self(sprintf('Unable to create a proxy for an abstract class "%s".', $className)); } /** * @param string $className * @psalm-param class-string $className * * @return self */ public static function classMustNotBeFinal($className) { return new self(sprintf('Unable to create a proxy for a final class "%s".', $className)); } /** * @param string $className * @psalm-param class-string $className * * @return self */ public static function classMustNotBeReadOnly($className) { return new self(sprintf('Unable to create a proxy for a readonly class "%s".', $className)); } /** @param mixed $value */ public static function invalidAutoGenerateMode($value): self { return new self(sprintf('Invalid auto generate mode "%s" given.', $value)); } } Proxy/ProxyDefinition.php 0000644 00000002160 15107332726 0011535 0 ustar 00 <?php namespace Doctrine\Common\Proxy; use ReflectionProperty; /** * Definition structure how to create a proxy. */ class ProxyDefinition { /** @var string */ public $proxyClassName; /** @var array<string> */ public $identifierFields; /** @var ReflectionProperty[] */ public $reflectionFields; /** @var callable */ public $initializer; /** @var callable */ public $cloner; /** * @param string $proxyClassName * @param array<string> $identifierFields * @param array<string, ReflectionProperty> $reflectionFields * @param callable $initializer * @param callable $cloner */ public function __construct($proxyClassName, array $identifierFields, array $reflectionFields, $initializer, $cloner) { $this->proxyClassName = $proxyClassName; $this->identifierFields = $identifierFields; $this->reflectionFields = $reflectionFields; $this->initializer = $initializer; $this->cloner = $cloner; } } Proxy/Proxy.php 0000644 00000003407 15107332726 0007531 0 ustar 00 <?php namespace Doctrine\Common\Proxy; use Closure; use Doctrine\Persistence\Proxy as BaseProxy; /** * Interface for proxy classes. * * @template T of object * @template-extends BaseProxy<T> */ interface Proxy extends BaseProxy { /** * Marks the proxy as initialized or not. * * @param bool $initialized * * @return void */ public function __setInitialized($initialized); /** * Sets the initializer callback to be used when initializing the proxy. That * initializer should accept 3 parameters: $proxy, $method and $params. Those * are respectively the proxy object that is being initialized, the method name * that triggered initialization and the parameters passed to that method. * * @return void */ public function __setInitializer(?Closure $initializer = null); /** * Retrieves the initializer callback used to initialize the proxy. * * @see __setInitializer * * @return Closure|null */ public function __getInitializer(); /** * Sets the callback to be used when cloning the proxy. That initializer should accept * a single parameter, which is the cloned proxy instance itself. * * @return void */ public function __setCloner(?Closure $cloner = null); /** * Retrieves the callback to be used when cloning the proxy. * * @see __setCloner * * @return Closure|null */ public function __getCloner(); /** * Retrieves the list of lazy loaded properties for a given proxy * * @return array<string, mixed> Keys are the property names, and values are the default values * for those properties. */ public function __getLazyProperties(); } Proxy/Autoloader.php 0000644 00000005617 15107332726 0010514 0 ustar 00 <?php namespace Doctrine\Common\Proxy; use Closure; use Doctrine\Common\Proxy\Exception\InvalidArgumentException; use function call_user_func; use function file_exists; use function is_callable; use function ltrim; use function spl_autoload_register; use function str_replace; use function strlen; use function strpos; use function substr; use const DIRECTORY_SEPARATOR; /** * Special Autoloader for Proxy classes, which are not PSR-0 compliant. * * @internal */ class Autoloader { /** * Resolves proxy class name to a filename based on the following pattern. * * 1. Remove Proxy namespace from class name. * 2. Remove namespace separators from remaining class name. * 3. Return PHP filename from proxy-dir with the result from 2. * * @param string $proxyDir * @param string $proxyNamespace * @param string $className * @psalm-param class-string $className * * @return string * * @throws InvalidArgumentException */ public static function resolveFile($proxyDir, $proxyNamespace, $className) { if (strpos($className, $proxyNamespace) !== 0) { throw InvalidArgumentException::notProxyClass($className, $proxyNamespace); } // remove proxy namespace from class name $classNameRelativeToProxyNamespace = substr($className, strlen($proxyNamespace)); // remove namespace separators from remaining class name $fileName = str_replace('\\', '', $classNameRelativeToProxyNamespace); return $proxyDir . DIRECTORY_SEPARATOR . $fileName . '.php'; } /** * Registers and returns autoloader callback for the given proxy dir and namespace. * * @param string $proxyDir * @param string $proxyNamespace * @param callable|null $notFoundCallback Invoked when the proxy file is not found. * * @return Closure * * @throws InvalidArgumentException */ public static function register($proxyDir, $proxyNamespace, $notFoundCallback = null) { $proxyNamespace = ltrim($proxyNamespace, '\\'); if ($notFoundCallback !== null && ! is_callable($notFoundCallback)) { throw InvalidArgumentException::invalidClassNotFoundCallback($notFoundCallback); } $autoloader = static function ($className) use ($proxyDir, $proxyNamespace, $notFoundCallback) { if ($proxyNamespace === '') { return; } if (strpos($className, $proxyNamespace) !== 0) { return; } $file = Autoloader::resolveFile($proxyDir, $proxyNamespace, $className); if ($notFoundCallback && ! file_exists($file)) { call_user_func($notFoundCallback, $proxyDir, $proxyNamespace, $className); } require $file; }; spl_autoload_register($autoloader); return $autoloader; } } ClassLoader.php 0000644 00000020442 15107332726 0007461 0 ustar 00 <?php namespace Doctrine\Common; use function class_exists; use function interface_exists; use function is_array; use function is_file; use function reset; use function spl_autoload_functions; use function spl_autoload_register; use function spl_autoload_unregister; use function str_replace; use function stream_resolve_include_path; use function strpos; use function trait_exists; use function trigger_error; use const DIRECTORY_SEPARATOR; use const E_USER_DEPRECATED; @trigger_error(ClassLoader::class . ' is deprecated.', E_USER_DEPRECATED); /** * A <tt>ClassLoader</tt> is an autoloader for class files that can be * installed on the SPL autoload stack. It is a class loader that either loads only classes * of a specific namespace or all namespaces and it is suitable for working together * with other autoloaders in the SPL autoload stack. * * If no include path is configured through the constructor or {@link setIncludePath}, a ClassLoader * relies on the PHP <code>include_path</code>. * * @deprecated The ClassLoader is deprecated and will be removed in version 4.0 of doctrine/common. */ class ClassLoader { /** * PHP file extension. * * @var string */ protected $fileExtension = '.php'; /** * Current namespace. * * @var string|null */ protected $namespace; /** * Current include path. * * @var string|null */ protected $includePath; /** * PHP namespace separator. * * @var string */ protected $namespaceSeparator = '\\'; /** * Creates a new <tt>ClassLoader</tt> that loads classes of the * specified namespace from the specified include path. * * If no include path is given, the ClassLoader relies on the PHP include_path. * If neither a namespace nor an include path is given, the ClassLoader will * be responsible for loading all classes, thereby relying on the PHP include_path. * * @param string|null $ns The namespace of the classes to load. * @param string|null $includePath The base include path to use. */ public function __construct($ns = null, $includePath = null) { $this->namespace = $ns; $this->includePath = $includePath; } /** * Sets the namespace separator used by classes in the namespace of this ClassLoader. * * @param string $sep The separator to use. * * @return void */ public function setNamespaceSeparator($sep) { $this->namespaceSeparator = $sep; } /** * Gets the namespace separator used by classes in the namespace of this ClassLoader. * * @return string */ public function getNamespaceSeparator() { return $this->namespaceSeparator; } /** * Sets the base include path for all class files in the namespace of this ClassLoader. * * @param string|null $includePath * * @return void */ public function setIncludePath($includePath) { $this->includePath = $includePath; } /** * Gets the base include path for all class files in the namespace of this ClassLoader. * * @return string|null */ public function getIncludePath() { return $this->includePath; } /** * Sets the file extension of class files in the namespace of this ClassLoader. * * @param string $fileExtension * * @return void */ public function setFileExtension($fileExtension) { $this->fileExtension = $fileExtension; } /** * Gets the file extension of class files in the namespace of this ClassLoader. * * @return string */ public function getFileExtension() { return $this->fileExtension; } /** * Registers this ClassLoader on the SPL autoload stack. * * @return void */ public function register() { spl_autoload_register([$this, 'loadClass']); } /** * Removes this ClassLoader from the SPL autoload stack. * * @return void */ public function unregister() { spl_autoload_unregister([$this, 'loadClass']); } /** * Loads the given class or interface. * * @param string $className The name of the class to load. * @psalm-param class-string $className * * @return bool TRUE if the class has been successfully loaded, FALSE otherwise. */ public function loadClass($className) { if (self::typeExists($className)) { return true; } if (! $this->canLoadClass($className)) { return false; } require($this->includePath !== null ? $this->includePath . DIRECTORY_SEPARATOR : '') . str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) . $this->fileExtension; return self::typeExists($className); } /** * Asks this ClassLoader whether it can potentially load the class (file) with * the given name. * * @param string $className The fully-qualified name of the class. * @psalm-param class-string $className * * @return bool TRUE if this ClassLoader can load the class, FALSE otherwise. */ public function canLoadClass($className) { if ($this->namespace !== null && strpos($className, $this->namespace . $this->namespaceSeparator) !== 0) { return false; } $file = str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) . $this->fileExtension; if ($this->includePath !== null) { return is_file($this->includePath . DIRECTORY_SEPARATOR . $file); } return stream_resolve_include_path($file) !== false; } /** * Checks whether a class with a given name exists. A class "exists" if it is either * already defined in the current request or if there is an autoloader on the SPL * autoload stack that is a) responsible for the class in question and b) is able to * load a class file in which the class definition resides. * * If the class is not already defined, each autoloader in the SPL autoload stack * is asked whether it is able to tell if the class exists. If the autoloader is * a <tt>ClassLoader</tt>, {@link canLoadClass} is used, otherwise the autoload * function of the autoloader is invoked and expected to return a value that * evaluates to TRUE if the class (file) exists. As soon as one autoloader reports * that the class exists, TRUE is returned. * * Note that, depending on what kinds of autoloaders are installed on the SPL * autoload stack, the class (file) might already be loaded as a result of checking * for its existence. This is not the case with a <tt>ClassLoader</tt>, who separates * these responsibilities. * * @param string $className The fully-qualified name of the class. * @psalm-param class-string $className * * @return bool TRUE if the class exists as per the definition given above, FALSE otherwise. */ public static function classExists($className) { return self::typeExists($className, true); } /** * Gets the <tt>ClassLoader</tt> from the SPL autoload stack that is responsible * for (and is able to load) the class with the given name. * * @param string $className The name of the class. * @psalm-param class-string $className * * @return ClassLoader|null The <tt>ClassLoader</tt> for the class or NULL if no such <tt>ClassLoader</tt> exists. */ public static function getClassLoader($className) { foreach (spl_autoload_functions() as $loader) { if (! is_array($loader)) { continue; } $classLoader = reset($loader); if ($classLoader instanceof ClassLoader && $classLoader->canLoadClass($className)) { return $classLoader; } } return null; } /** * Checks whether a given type exists * * @param string $type * @param bool $autoload * * @return bool */ private static function typeExists($type, $autoload = false) { return class_exists($type, $autoload) || interface_exists($type, $autoload) || trait_exists($type, $autoload); } } Util/ClassUtils.php 0000644 00000005310 15107332726 0010265 0 ustar 00 <?php namespace Doctrine\Common\Util; use Doctrine\Persistence\Proxy; use ReflectionClass; use function get_class; use function get_parent_class; use function ltrim; use function rtrim; use function strrpos; use function substr; /** * Class and reflection related functionality for objects that * might or not be proxy objects at the moment. */ class ClassUtils { /** * Gets the real class name of a class name that could be a proxy. * * @param string $className * @psalm-param class-string<Proxy<T>>|class-string<T> $className * * @return string * @psalm-return class-string<T> * * @template T of object */ public static function getRealClass($className) { $pos = strrpos($className, '\\' . Proxy::MARKER . '\\'); if ($pos === false) { /** @psalm-var class-string<T> */ return $className; } return substr($className, $pos + Proxy::MARKER_LENGTH + 2); } /** * Gets the real class name of an object (even if its a proxy). * * @param object $object * @psalm-param Proxy<T>|T $object * * @return string * @psalm-return class-string<T> * * @template T of object */ public static function getClass($object) { return self::getRealClass(get_class($object)); } /** * Gets the real parent class name of a class or object. * * @param string $className * @psalm-param class-string $className * * @return string * @psalm-return class-string */ public static function getParentClass($className) { return get_parent_class(self::getRealClass($className)); } /** * Creates a new reflection class. * * @param string $className * @psalm-param class-string $className * * @return ReflectionClass */ public static function newReflectionClass($className) { return new ReflectionClass(self::getRealClass($className)); } /** * Creates a new reflection object. * * @param object $object * * @return ReflectionClass */ public static function newReflectionObject($object) { return self::newReflectionClass(self::getClass($object)); } /** * Given a class name and a proxy namespace returns the proxy name. * * @param string $className * @param string $proxyNamespace * @psalm-param class-string $className * * @return string * @psalm-return class-string */ public static function generateProxyClassName($className, $proxyNamespace) { return rtrim($proxyNamespace, '\\') . '\\' . Proxy::MARKER . '\\' . ltrim($className, '\\'); } } Util/Debug.php 0000644 00000011151 15107332726 0007225 0 ustar 00 <?php namespace Doctrine\Common\Util; use ArrayIterator; use ArrayObject; use DateTimeInterface; use Doctrine\Common\Collections\Collection; use Doctrine\Persistence\Proxy; use stdClass; use function array_keys; use function count; use function end; use function explode; use function extension_loaded; use function get_class; use function html_entity_decode; use function ini_get; use function ini_set; use function is_array; use function is_object; use function method_exists; use function ob_end_clean; use function ob_get_contents; use function ob_start; use function spl_object_hash; use function strip_tags; use function var_dump; /** * Static class containing most used debug methods. * * @deprecated The Debug class is deprecated, please use symfony/var-dumper instead. * * @link www.doctrine-project.org */ final class Debug { /** * Private constructor (prevents instantiation). */ private function __construct() { } /** * Prints a dump of the public, protected and private properties of $var. * * @link https://xdebug.org/ * * @param mixed $var The variable to dump. * @param int $maxDepth The maximum nesting level for object properties. * @param bool $stripTags Whether output should strip HTML tags. * @param bool $echo Send the dumped value to the output buffer * * @return string */ public static function dump($var, $maxDepth = 2, $stripTags = true, $echo = true) { $html = ini_get('html_errors'); if ($html !== true) { ini_set('html_errors', 'on'); } if (extension_loaded('xdebug')) { ini_set('xdebug.var_display_max_depth', $maxDepth); } $var = self::export($var, $maxDepth); ob_start(); var_dump($var); $dump = ob_get_contents(); ob_end_clean(); $dumpText = ($stripTags ? strip_tags(html_entity_decode($dump)) : $dump); ini_set('html_errors', $html); if ($echo) { echo $dumpText; } return $dumpText; } /** * @param mixed $var * @param int $maxDepth * * @return mixed */ public static function export($var, $maxDepth) { $return = null; $isObj = is_object($var); if ($var instanceof Collection) { $var = $var->toArray(); } if (! $maxDepth) { return is_object($var) ? get_class($var) : (is_array($var) ? 'Array(' . count($var) . ')' : $var); } if (is_array($var)) { $return = []; foreach ($var as $k => $v) { $return[$k] = self::export($v, $maxDepth - 1); } return $return; } if (! $isObj) { return $var; } $return = new stdClass(); if ($var instanceof DateTimeInterface) { $return->__CLASS__ = get_class($var); $return->date = $var->format('c'); $return->timezone = $var->getTimezone()->getName(); return $return; } $return->__CLASS__ = ClassUtils::getClass($var); if ($var instanceof Proxy) { $return->__IS_PROXY__ = true; $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); } if ($var instanceof ArrayObject || $var instanceof ArrayIterator) { $return->__STORAGE__ = self::export($var->getArrayCopy(), $maxDepth - 1); } return self::fillReturnWithClassAttributes($var, $return, $maxDepth); } /** * Fill the $return variable with class attributes * Based on obj2array function from {@see https://secure.php.net/manual/en/function.get-object-vars.php#47075} * * @param object $var * @param int $maxDepth * * @return mixed */ private static function fillReturnWithClassAttributes($var, stdClass $return, $maxDepth) { $clone = (array) $var; foreach (array_keys($clone) as $key) { $aux = explode("\0", $key); $name = end($aux); if ($aux[0] === '') { $name .= ':' . ($aux[1] === '*' ? 'protected' : $aux[1] . ':private'); } $return->$name = self::export($clone[$key], $maxDepth - 1); } return $return; } /** * Returns a string representation of an object. * * @param object $obj * * @return string */ public static function toString($obj) { return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj); } } CommonException.php 0000644 00000000424 15107332726 0010372 0 ustar 00 <?php namespace Doctrine\Common; use Exception; /** * Base exception class for package Doctrine\Common. * * @deprecated The doctrine/common package is deprecated, please use specific packages and their exceptions instead. */ class CommonException extends Exception { } Comparable.php 0000644 00000001315 15107332726 0007330 0 ustar 00 <?php namespace Doctrine\Common; /** * Comparable interface that allows to compare two value objects to each other for similarity. * * @link www.doctrine-project.org */ interface Comparable { /** * Compares the current object to the passed $other. * * Returns 0 if they are semantically equal, 1 if the other object * is less than the current one, or -1 if its more than the current one. * * This method should not check for identity using ===, only for semantical equality for example * when two different DateTime instances point to the exact same Date + TZ. * * @param mixed $other * * @return int */ public function compareTo($other); } exceptions/RuntimeException.php 0000644 00000000654 15107341044 0012744 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of phpunit/php-text-template. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use InvalidArgumentException; final class RuntimeException extends InvalidArgumentException implements Exception { } exceptions/InvalidArgumentException.php 0000644 00000000626 15107341045 0014412 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of phpunit/php-text-template. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; final class InvalidArgumentException extends \InvalidArgumentException implements Exception { } Template.php 0000644 00000004765 15107341046 0007045 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of phpunit/php-text-template. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Template; use function array_keys; use function array_merge; use function file_get_contents; use function file_put_contents; use function is_file; use function sprintf; use function str_replace; final class Template { private string $template = ''; private string $openDelimiter; private string $closeDelimiter; /** * @psalm-var array<string,string> */ private array $values = []; /** * @throws InvalidArgumentException */ public function __construct(string $file = '', string $openDelimiter = '{', string $closeDelimiter = '}') { $this->setFile($file); $this->openDelimiter = $openDelimiter; $this->closeDelimiter = $closeDelimiter; } /** * @throws InvalidArgumentException */ public function setFile(string $file): void { if (is_file($file)) { $this->template = file_get_contents($file); return; } $distFile = $file . '.dist'; if (is_file($distFile)) { $this->template = file_get_contents($distFile); return; } throw new InvalidArgumentException( sprintf( 'Failed to load template "%s"', $file ) ); } /** * @psalm-param array<string,string> $values */ public function setVar(array $values, bool $merge = true): void { if (!$merge || empty($this->values)) { $this->values = $values; return; } $this->values = array_merge($this->values, $values); } public function render(): string { $keys = []; foreach (array_keys($this->values) as $key) { $keys[] = $this->openDelimiter . $key . $this->closeDelimiter; } return str_replace($keys, $this->values, $this->template); } /** * @codeCoverageIgnore */ public function renderTo(string $target): void { if (!@file_put_contents($target, $this->render())) { throw new RuntimeException( sprintf( 'Writing rendered result to "%s" failed', $target ) ); } } }
Simpan