File manager - Edit - /home/autoph/public_html/projects/api/public/src.tar
Back
NotFoundExceptionInterface.php 0000644 00000000236 15025035457 0012520 0 ustar 00 <?php namespace Psr\Container; /** * No entry was found in the container. */ interface NotFoundExceptionInterface extends ContainerExceptionInterface { } ContainerExceptionInterface.php 0000644 00000000270 15025035457 0012704 0 ustar 00 <?php namespace Psr\Container; use Throwable; /** * Base interface representing a generic exception in a container. */ interface ContainerExceptionInterface extends Throwable { } ContainerInterface.php 0000644 00000002026 15025035457 0011026 0 ustar 00 <?php declare(strict_types=1); namespace Psr\Container; /** * Describes the interface of a container that exposes methods to read its entries. */ interface ContainerInterface { /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get(string $id); /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has(string $id): bool; } Set.php 0000644 00000003364 15025061402 0006011 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 don't exists * $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 { /** * The type of elements stored in this set * * A set's type is immutable. For this reason, this property is private. * * @var string */ private $setType; /** * Constructs a set object of the specified type, optionally with the * specified data. * * @param string $setType The type (FQCN) associated with this set. * @param array<array-key, T> $data The initial items to store in the set. */ public function __construct(string $setType, array $data = []) { $this->setType = $setType; parent::__construct($data); } public function getType(): string { return $this->setType; } } AbstractArray.php 0000644 00000013001 15025061402 0010005 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 serialize; use function unserialize; /** * 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 $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($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|null the value stored at the offset, or null if the offset * does not exist. * * @psalm-suppress InvalidAttribute */ #[\ReturnTypeWillChange] // phpcs:ignore public function offsetGet($offset) { return $this->data[$offset] ?? null; } /** * 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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function offsetSet($offset, $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($offset): void { unset($this->data[$offset]); } /** * Returns a serialized string representation of this array object. * * @deprecated The Serializable interface will go away in PHP 9. * * @link http://php.net/manual/en/serializable.serialize.php Serializable::serialize() * * @return string a PHP serialized string. */ public function serialize(): string { return serialize($this->data); } /** * 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; } /** * Converts a serialized string representation into an instance object. * * @deprecated The Serializable interface will go away in PHP 9. * * @link http://php.net/manual/en/serializable.unserialize.php Serializable::unserialize() * * @param string $serialized A PHP serialized string to unserialize. * * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint */ public function unserialize($serialized): void { /** @var array<array-key, T> $data */ $data = unserialize($serialized, ['allowed_classes' => false]); $this->data = $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 count($this->data) === 0; } } CollectionInterface.php 0000644 00000015303 15025061402 0011166 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, 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 { /** * Ascending sort type. */ public const SORT_ASC = 'asc'; /** * Descending sort type. */ public const SORT_DESC = 'desc'; /** * 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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function add($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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function contains($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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function remove($element): bool; /** * Returns the values from the given property or method. * * @param string $propertyOrMethod The property or method name to filter by. * * @return list<mixed> */ public function column(string $propertyOrMethod): array; /** * Returns the first item of the collection. * * @return T */ public function first(); /** * Returns the last item of the collection. * * @return T */ public function last(); /** * Sort the collection by a property or method with the given sort order. * * This will always leave the original collection untouched and will return * a new one. * * @param string $propertyOrMethod The property or method to sort by. * @param string $order The sort order for the resulting collection (one of * this interface's `SORT_*` constants). * * @return CollectionInterface<T> */ public function sort(string $propertyOrMethod, string $order = self::SORT_ASC): 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 items match the criteria of given callback. * * This will always leave the original collection untouched and will return * a new one. * * @param string $propertyOrMethod The property or method to evaluate. * @param mixed $value The value to match. * * @return CollectionInterface<T> */ public function where(string $propertyOrMethod, $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; /** * 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> */ 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> */ 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> */ public function merge(CollectionInterface ...$collections): self; } Queue.php 0000644 00000007325 15025061402 0006343 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; /** * 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; /** * The type of elements stored in this queue. * * A queue's type is immutable once it is set. For this reason, this * property is set private. * * @var string */ private $queueType; /** * The index of the head of the queue. * * @var int */ protected $index = 0; /** * Constructs a queue object of the specified type, optionally with the * specified data. * * @param string $queueType The type (FQCN) associated with this queue. * @param array<array-key, T> $data The initial items to store in the collection. */ public function __construct(string $queueType, array $data = []) { $this->queueType = $queueType; 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. */ public function offsetSet($offset, $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; } /** * @inheritDoc */ public function add($element): bool { $this[] = $element; return true; } /** * @inheritDoc */ public function element() { $element = $this->peek(); if ($element === null) { throw new NoSuchElementException( 'Can\'t return element from Queue. Queue is empty.' ); } return $element; } /** * @inheritDoc */ public function offer($element): bool { try { return $this->add($element); } catch (InvalidArgumentException $e) { return false; } } /** * @inheritDoc */ public function peek() { if ($this->count() === 0) { return null; } return $this[$this->index]; } /** * @inheritDoc */ public function poll() { if ($this->count() === 0) { return null; } $head = $this[$this->index]; unset($this[$this->index]); $this->index++; return $head; } /** * @inheritDoc */ public function remove() { $head = $this->poll(); if ($head === null) { throw new NoSuchElementException('Can\'t return element from Queue. Queue is empty.'); } return $head; } public function getType(): string { return $this->queueType; } } Tool/TypeTrait.php 0000644 00000003650 15025061402 0010116 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, $value): bool { switch ($type) { case 'array': return is_array($value); case 'bool': case 'boolean': return is_bool($value); case 'callable': return is_callable($value); case 'float': case 'double': return is_float($value); case 'int': case 'integer': return is_int($value); case 'null': return $value === null; case 'numeric': return is_numeric($value); case 'object': return is_object($value); case 'resource': return is_resource($value); case 'scalar': return is_scalar($value); case 'string': return is_string($value); case 'mixed': return true; default: return $value instanceof $type; } } } Tool/ValueToStringTrait.php 0000644 00000004527 15025061402 0011747 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 get_class; use function get_resource_type; use function is_array; use function is_bool; use function is_callable; 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($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 . ')'; } // If we don't know what it is, use var_export(). if (!is_object($value)) { return '(' . var_export($value, true) . ')'; } // From here, $value should be an object. // __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 '(' . get_class($value) . ' Object)'; } } Tool/ValueExtractorTrait.php 0000644 00000003310 15025061402 0012136 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\ValueExtractionException; use function get_class; 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 or method from the object. * * @param mixed $object The object to extract the value from. * @param string $propertyOrMethod The property or method for which the * value should be extracted. * * @return mixed the value extracted from the specified property or method. * * @throws ValueExtractionException if the method or property is not defined. */ protected function extractValue($object, string $propertyOrMethod) { if (!is_object($object)) { throw new ValueExtractionException('Unable to extract a value from a non-object'); } if (property_exists($object, $propertyOrMethod)) { return $object->$propertyOrMethod; } if (method_exists($object, $propertyOrMethod)) { return $object->{$propertyOrMethod}(); } throw new ValueExtractionException( sprintf('Method or property "%s" not defined in %s', $propertyOrMethod, get_class($object)) ); } } DoubleEndedQueueInterface.php 0000644 00000024601 15025061402 0012253 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; /** * 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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function addFirst($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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function addLast($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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function offerFirst($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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function offerLast($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(); /** * 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(); /** * 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(); /** * 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(); /** * 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(); /** * 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(); /** * 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(); /** * 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(); } Exception/NoSuchElementException.php 0000644 00000001002 15025061402 0013567 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; /** * Thrown when attempting to access an element that does not exist. */ class NoSuchElementException extends \RuntimeException { } Exception/UnsupportedOperationException.php 0000644 00000001012 15025061402 0015270 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; /** * Thrown to indicate that the requested operation is not supported. */ class UnsupportedOperationException extends \RuntimeException { } Exception/CollectionMismatchException.php 0000644 00000001013 15025061402 0014641 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; /** * Thrown when attempting to operate on collections of differing types. */ class CollectionMismatchException extends \RuntimeException { } Exception/OutOfBoundsException.php 0000644 00000001023 15025061402 0013270 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; /** * Thrown when attempting to access an element out of the range of the collection. */ class OutOfBoundsException extends \OutOfBoundsException { } Exception/InvalidSortOrderException.php 0000644 00000001007 15025061402 0014315 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; /** * Thrown when attempting to use a sort order that is not recognized. */ class InvalidSortOrderException extends \RuntimeException { } Exception/ValueExtractionException.php 0000644 00000001033 15025061402 0014177 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; /** * Thrown when attempting to extract a value for a method or property that does not exist. */ class ValueExtractionException extends \RuntimeException { } Exception/InvalidArgumentException.php 0000644 00000000203 15025061402 0014151 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\Exception; class InvalidArgumentException extends JsonMachineException { } AbstractSet.php 0000644 00000002115 15025061402 0007466 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 { /** * @inheritDoc */ public function add($element): bool { if ($this->contains($element)) { return false; } return parent::add($element); } /** * @inheritDoc */ public function offsetSet($offset, $value): void { if ($this->contains($value)) { return; } parent::offsetSet($offset, $value); } } Collection.php 0000644 00000005050 15025061402 0007343 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 { /** * The type of elements stored in this collection. * * A collection's type is immutable once it is set. For this reason, this * property is set private. * * @var string */ private $collectionType; /** * Constructs a collection object of the specified type, optionally with the * specified data. * * @param string $collectionType The type (FQCN) associated with this * collection. * @param array<array-key, T> $data The initial items to store in the collection. */ public function __construct(string $collectionType, array $data = []) { $this->collectionType = $collectionType; parent::__construct($data); } public function getType(): string { return $this->collectionType; } } DoubleEndedQueue.php 0000644 00000007261 15025061402 0010435 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; /** * 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 { /** * Index of the last element in the queue. * * @var int */ private $tail = -1; /** * @inheritDoc */ public function offsetSet($offset, $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->tail++; $this->data[$this->tail] = $value; } /** * @inheritDoc */ public function addFirst($element): bool { if ($this->checkType($this->getType(), $element) === false) { throw new InvalidArgumentException( 'Value must be of type ' . $this->getType() . '; value is ' . $this->toolValueToString($element) ); } $this->index--; $this->data[$this->index] = $element; return true; } /** * @inheritDoc */ public function addLast($element): bool { return $this->add($element); } /** * @inheritDoc */ public function offerFirst($element): bool { try { return $this->addFirst($element); } catch (InvalidArgumentException $e) { return false; } } /** * @inheritDoc */ public function offerLast($element): bool { return $this->offer($element); } /** * @inheritDoc */ public function removeFirst() { return $this->remove(); } /** * @inheritDoc */ public function removeLast() { $tail = $this->pollLast(); if ($tail === null) { throw new NoSuchElementException('Can\'t return element from Queue. Queue is empty.'); } return $tail; } /** * @inheritDoc */ public function pollFirst() { return $this->poll(); } /** * @inheritDoc */ public function pollLast() { if ($this->count() === 0) { return null; } $tail = $this[$this->tail]; unset($this[$this->tail]); $this->tail--; return $tail; } /** * @inheritDoc */ public function firstElement() { return $this->element(); } /** * @inheritDoc */ public function lastElement() { if ($this->count() === 0) { throw new NoSuchElementException('Can\'t return element from Queue. Queue is empty.'); } return $this->data[$this->tail]; } /** * @inheritDoc */ public function peekFirst() { return $this->peek(); } /** * @inheritDoc */ public function peekLast() { if ($this->count() === 0) { return null; } return $this->data[$this->tail]; } } Map/AbstractMap.php 0000644 00000006206 15025061402 0010172 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 function array_key_exists; use function array_keys; use function in_array; /** * This class provides a basic implementation of `MapInterface`, to minimize the * effort required to implement this interface. * * @template T * @extends AbstractArray<T> * @implements MapInterface<T> */ abstract class AbstractMap extends AbstractArray implements MapInterface { /** * @inheritDoc */ public function offsetSet($offset, $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; } /** * @inheritDoc */ public function containsKey($key): bool { return array_key_exists($key, $this->data); } /** * @inheritDoc */ public function containsValue($value): bool { return in_array($value, $this->data, true); } /** * @inheritDoc */ public function keys(): array { return array_keys($this->data); } /** * @inheritDoc */ public function get($key, $defaultValue = null) { if (!$this->containsKey($key)) { return $defaultValue; } return $this[$key]; } /** * @inheritDoc */ public function put($key, $value) { $previousValue = $this->get($key); $this[$key] = $value; return $previousValue; } /** * @inheritDoc */ public function putIfAbsent($key, $value) { $currentValue = $this->get($key); if ($currentValue === null) { $this[$key] = $value; } return $currentValue; } /** * @inheritDoc */ public function remove($key) { $previousValue = $this->get($key); unset($this[$key]); return $previousValue; } /** * @inheritDoc */ public function removeIf($key, $value): bool { if ($this->get($key) === $value) { unset($this[$key]); return true; } return false; } /** * @inheritDoc */ public function replace($key, $value) { $currentValue = $this->get($key); if ($this->containsKey($key)) { $this[$key] = $value; } return $currentValue; } /** * @inheritDoc */ public function replaceIf($key, $oldValue, $newValue): bool { if ($this->get($key) === $oldValue) { $this[$key] = $newValue; return true; } return false; } } Map/NamedParameterMap.php 0000644 00000006423 15025061402 0011315 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<mixed> */ class NamedParameterMap extends AbstractMap { use TypeTrait; use ValueToStringTrait; /** * Named parameters defined for this map. * * @var array<string, string> */ protected $namedParameters; /** * Constructs a new `NamedParameterMap`. * * @param array<array-key, string> $namedParameters The named parameters defined for this map. * @param array<array-key, 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; } /** * @inheritDoc */ public function offsetSet($offset, $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) ); } if (!array_key_exists($offset, $this->namedParameters)) { throw new InvalidArgumentException( 'Attempting to set value for unconfigured parameter \'' . $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/AssociativeArrayMap.php 0000644 00000001045 15025061402 0011674 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. * * @template T * @extends AbstractMap<T> */ class AssociativeArrayMap extends AbstractMap { } Map/MapInterface.php 0000644 00000012040 15025061402 0010320 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 T * @extends ArrayInterface<T> */ interface MapInterface extends ArrayInterface { /** * Returns `true` if this map contains a mapping for the specified key. * * @param array-key $key The key to check in the map. */ public function containsKey($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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function containsValue($value): bool; /** * Return an array of the keys contained in this map. * * @return list<array-key> */ 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 array-key $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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function get($key, $defaultValue = null); /** * 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 array-key $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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function put($key, $value); /** * 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 array-key $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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function putIfAbsent($key, $value); /** * Removes the mapping for a key from this map if it is present. * * @param array-key $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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function remove($key); /** * 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 array-key $key The key to remove from the map. * @param T $value The value to match. * * @return bool true if the value was removed. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function removeIf($key, $value): bool; /** * Replaces the entry for the specified key only if it is currently mapped * to some value. * * @param array-key $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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function replace($key, $value); /** * 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 array-key $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. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function replaceIf($key, $oldValue, $newValue): bool; } Map/TypedMap.php 0000644 00000006611 15025061402 0007514 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\Tool\TypeTrait; /** * 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 * @template T * @extends AbstractTypedMap<K, T> */ class TypedMap extends AbstractTypedMap { use TypeTrait; /** * The data type of keys stored in this collection. * * A map key's type is immutable once it is set. For this reason, this * property is set private. * * @var string data type of the map key. */ private $keyType; /** * The data type of values stored in this collection. * * A map value's type is immutable once it is set. For this reason, this * property is set private. * * @var string data type of the map value. */ private $valueType; /** * 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(string $keyType, string $valueType, array $data = []) { $this->keyType = $keyType; $this->valueType = $valueType; /** @psalm-suppress MixedArgumentTypeCoercion */ parent::__construct($data); } public function getKeyType(): string { return $this->keyType; } public function getValueType(): string { return $this->valueType; } } Map/AbstractTypedMap.php 0000644 00000003723 15025061402 0011201 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 * @template T * @extends AbstractMap<T> * @implements TypedMapInterface<T> */ abstract class AbstractTypedMap extends AbstractMap implements TypedMapInterface { use TypeTrait; use ValueToStringTrait; /** * @param K|null $offset * @param T $value * * @inheritDoc * * @psalm-suppress MoreSpecificImplementedParamType */ public function offsetSet($offset, $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) ); } 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) ); } /** @psalm-suppress MixedArgumentTypeCoercion */ parent::offsetSet($offset, $value); } } Map/TypedMapInterface.php 0000644 00000001404 15025061402 0011330 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 T * @extends MapInterface<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; } QueueInterface.php 0000644 00000016526 15025061402 0010167 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; /** * 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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function add($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(); /** * 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`. */ // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint public function offer($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(); /** * 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(); /** * 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(); /** * Returns the type associated with this queue. */ public function getType(): string; } ArrayInterface.php 0000644 00000002111 15025061402 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; use ArrayAccess; use Countable; use IteratorAggregate; use Serializable; /** * `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, Serializable { /** * 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; } GenericArray.php 0000644 00000001000 15025061402 0007612 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 { } AbstractCollection.php 0000644 00000021515 15025061402 0011033 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\InvalidSortOrderException; use Ramsey\Collection\Exception\OutOfBoundsException; use Ramsey\Collection\Tool\TypeTrait; use Ramsey\Collection\Tool\ValueExtractorTrait; use Ramsey\Collection\Tool\ValueToStringTrait; use function array_filter; use function array_map; use function array_merge; use function array_search; use function array_udiff; use function array_uintersect; use function current; use function end; use function in_array; use function is_int; use function reset; use function sprintf; use function unserialize; 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; /** * @inheritDoc */ public function add($element): bool { $this[] = $element; return true; } /** * @inheritDoc */ public function contains($element, bool $strict = true): bool { return in_array($element, $this->data, $strict); } /** * @inheritDoc */ public function offsetSet($offset, $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; } } /** * @inheritDoc */ public function remove($element): bool { if (($position = array_search($element, $this->data, true)) !== false) { unset($this->data[$position]); return true; } return false; } /** * @inheritDoc */ public function column(string $propertyOrMethod): array { $temp = []; foreach ($this->data as $item) { /** @var mixed $value */ $value = $this->extractValue($item, $propertyOrMethod); /** @psalm-suppress MixedAssignment */ $temp[] = $value; } return $temp; } /** * @inheritDoc */ public function first() { if ($this->isEmpty()) { throw new OutOfBoundsException('Can\'t determine first item. Collection is empty'); } reset($this->data); /** @var T $first */ $first = current($this->data); return $first; } /** * @inheritDoc */ public function last() { if ($this->isEmpty()) { throw new OutOfBoundsException('Can\'t determine last item. Collection is empty'); } /** @var T $item */ $item = end($this->data); reset($this->data); return $item; } public function sort(string $propertyOrMethod, string $order = self::SORT_ASC): CollectionInterface { if (!in_array($order, [self::SORT_ASC, self::SORT_DESC], true)) { throw new InvalidSortOrderException('Invalid sort order given: ' . $order); } $collection = clone $this; usort( $collection->data, /** * @param T $a * @param T $b */ function ($a, $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 === self::SORT_DESC ? -1 : 1); } ); return $collection; } public function filter(callable $callback): CollectionInterface { $collection = clone $this; $collection->data = array_merge([], array_filter($collection->data, $callback)); return $collection; } /** * {@inheritdoc} */ public function where(string $propertyOrMethod, $value): CollectionInterface { return $this->filter(function ($item) use ($propertyOrMethod, $value) { /** @var mixed $accessorValue */ $accessorValue = $this->extractValue($item, $propertyOrMethod); return $accessorValue === $value; }); } public function map(callable $callback): CollectionInterface { return new Collection('mixed', array_map($callback, $this->data)); } 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; } 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; } 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 ($collection->getType() !== $this->getType()) { 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; } /** * @inheritDoc */ public function unserialize($serialized): void { /** @var array<array-key, T> $data */ $data = unserialize($serialized, ['allowed_classes' => [$this->getType()]]); $this->data = $data; } /** * @param CollectionInterface<T> $other */ 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 ($other->getType() !== $this->getType()) { throw new CollectionMismatchException('Collection items must be of type ' . $this->getType()); } } private function getComparator(): Closure { return /** * @param T $a * @param T $b */ function ($a, $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); }; } } Internal/Calculator/NativeCalculator.php 0000644 00000034660 15025063510 0014370 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator; /** * Calculator implementation using only native PHP code. * * @internal * * @psalm-immutable */ class NativeCalculator extends Calculator { /** * The max number of digits the platform can natively add, subtract, multiply or divide without overflow. * For multiplication, this represents the max sum of the lengths of both operands. * * For addition, it is assumed that an extra digit can hold a carry (1) without overflowing. * Example: 32-bit: max number 1,999,999,999 (9 digits + carry) * 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry) * * @var int */ private $maxDigits; /** * Class constructor. * * @codeCoverageIgnore */ public function __construct() { switch (PHP_INT_SIZE) { case 4: $this->maxDigits = 9; break; case 8: $this->maxDigits = 18; break; default: throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.'); } } /** * {@inheritdoc} */ public function add(string $a, string $b) : string { /** * @psalm-var numeric-string $a * @psalm-var numeric-string $b */ $result = $a + $b; if (is_int($result)) { return (string) $result; } if ($a === '0') { return $b; } if ($b === '0') { return $a; } [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig); if ($aNeg) { $result = $this->neg($result); } return $result; } /** * {@inheritdoc} */ public function sub(string $a, string $b) : string { return $this->add($a, $this->neg($b)); } /** * {@inheritdoc} */ public function mul(string $a, string $b) : string { /** * @psalm-var numeric-string $a * @psalm-var numeric-string $b */ $result = $a * $b; if (is_int($result)) { return (string) $result; } if ($a === '0' || $b === '0') { return '0'; } if ($a === '1') { return $b; } if ($b === '1') { return $a; } if ($a === '-1') { return $this->neg($b); } if ($b === '-1') { return $this->neg($a); } [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); $result = $this->doMul($aDig, $bDig); if ($aNeg !== $bNeg) { $result = $this->neg($result); } return $result; } /** * {@inheritdoc} */ public function divQ(string $a, string $b) : string { return $this->divQR($a, $b)[0]; } /** * {@inheritdoc} */ public function divR(string $a, string $b): string { return $this->divQR($a, $b)[1]; } /** * {@inheritdoc} */ public function divQR(string $a, string $b) : array { if ($a === '0') { return ['0', '0']; } if ($a === $b) { return ['1', '0']; } if ($b === '1') { return [$a, '0']; } if ($b === '-1') { return [$this->neg($a), '0']; } /** @psalm-var numeric-string $a */ $na = $a * 1; // cast to number if (is_int($na)) { /** @psalm-var numeric-string $b */ $nb = $b * 1; if (is_int($nb)) { // the only division that may overflow is PHP_INT_MIN / -1, // which cannot happen here as we've already handled a divisor of -1 above. $r = $na % $nb; $q = ($na - $r) / $nb; assert(is_int($q)); return [ (string) $q, (string) $r ]; } } [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); [$q, $r] = $this->doDiv($aDig, $bDig); if ($aNeg !== $bNeg) { $q = $this->neg($q); } if ($aNeg) { $r = $this->neg($r); } return [$q, $r]; } /** * {@inheritdoc} */ public function pow(string $a, int $e) : string { if ($e === 0) { return '1'; } if ($e === 1) { return $a; } $odd = $e % 2; $e -= $odd; $aa = $this->mul($a, $a); /** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */ $result = $this->pow($aa, $e / 2); if ($odd === 1) { $result = $this->mul($result, $a); } return $result; } /** * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ * * {@inheritdoc} */ public function modPow(string $base, string $exp, string $mod) : string { // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) if ($base === '0' && $exp === '0' && $mod === '1') { return '0'; } // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0) if ($exp === '0' && $mod === '1') { return '0'; } $x = $base; $res = '1'; // numbers are positive, so we can use remainder instead of modulo $x = $this->divR($x, $mod); while ($exp !== '0') { if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd $res = $this->divR($this->mul($res, $x), $mod); } $exp = $this->divQ($exp, '2'); $x = $this->divR($this->mul($x, $x), $mod); } return $res; } /** * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html * * {@inheritDoc} */ public function sqrt(string $n) : string { if ($n === '0') { return '0'; } // initial approximation $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1); $decreased = false; for (;;) { $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2'); if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) { break; } $decreased = $this->cmp($nx, $x) < 0; $x = $nx; } return $x; } /** * Performs the addition of two non-signed large integers. * * @param string $a The first operand. * @param string $b The second operand. * * @return string */ private function doAdd(string $a, string $b) : string { [$a, $b, $length] = $this->pad($a, $b); $carry = 0; $result = ''; for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { $blockLength = $this->maxDigits; if ($i < 0) { $blockLength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; } /** @psalm-var numeric-string $blockA */ $blockA = \substr($a, $i, $blockLength); /** @psalm-var numeric-string $blockB */ $blockB = \substr($b, $i, $blockLength); $sum = (string) ($blockA + $blockB + $carry); $sumLength = \strlen($sum); if ($sumLength > $blockLength) { $sum = \substr($sum, 1); $carry = 1; } else { if ($sumLength < $blockLength) { $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; } $carry = 0; } $result = $sum . $result; if ($i === 0) { break; } } if ($carry === 1) { $result = '1' . $result; } return $result; } /** * Performs the subtraction of two non-signed large integers. * * @param string $a The first operand. * @param string $b The second operand. * * @return string */ private function doSub(string $a, string $b) : string { if ($a === $b) { return '0'; } // Ensure that we always subtract to a positive result: biggest minus smallest. $cmp = $this->doCmp($a, $b); $invert = ($cmp === -1); if ($invert) { $c = $a; $a = $b; $b = $c; } [$a, $b, $length] = $this->pad($a, $b); $carry = 0; $result = ''; $complement = 10 ** $this->maxDigits; for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { $blockLength = $this->maxDigits; if ($i < 0) { $blockLength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; } /** @psalm-var numeric-string $blockA */ $blockA = \substr($a, $i, $blockLength); /** @psalm-var numeric-string $blockB */ $blockB = \substr($b, $i, $blockLength); $sum = $blockA - $blockB - $carry; if ($sum < 0) { $sum += $complement; $carry = 1; } else { $carry = 0; } $sum = (string) $sum; $sumLength = \strlen($sum); if ($sumLength < $blockLength) { $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; } $result = $sum . $result; if ($i === 0) { break; } } // Carry cannot be 1 when the loop ends, as a > b assert($carry === 0); $result = \ltrim($result, '0'); if ($invert) { $result = $this->neg($result); } return $result; } /** * Performs the multiplication of two non-signed large integers. * * @param string $a The first operand. * @param string $b The second operand. * * @return string */ private function doMul(string $a, string $b) : string { $x = \strlen($a); $y = \strlen($b); $maxDigits = \intdiv($this->maxDigits, 2); $complement = 10 ** $maxDigits; $result = '0'; for ($i = $x - $maxDigits;; $i -= $maxDigits) { $blockALength = $maxDigits; if ($i < 0) { $blockALength += $i; /** @psalm-suppress LoopInvalidation */ $i = 0; } $blockA = (int) \substr($a, $i, $blockALength); $line = ''; $carry = 0; for ($j = $y - $maxDigits;; $j -= $maxDigits) { $blockBLength = $maxDigits; if ($j < 0) { $blockBLength += $j; /** @psalm-suppress LoopInvalidation */ $j = 0; } $blockB = (int) \substr($b, $j, $blockBLength); $mul = $blockA * $blockB + $carry; $value = $mul % $complement; $carry = ($mul - $value) / $complement; $value = (string) $value; $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT); $line = $value . $line; if ($j === 0) { break; } } if ($carry !== 0) { $line = $carry . $line; } $line = \ltrim($line, '0'); if ($line !== '') { $line .= \str_repeat('0', $x - $blockALength - $i); $result = $this->add($result, $line); } if ($i === 0) { break; } } return $result; } /** * Performs the division of two non-signed large integers. * * @param string $a The first operand. * @param string $b The second operand. * * @return string[] The quotient and remainder. */ private function doDiv(string $a, string $b) : array { $cmp = $this->doCmp($a, $b); if ($cmp === -1) { return ['0', $a]; } $x = \strlen($a); $y = \strlen($b); // we now know that a >= b && x >= y $q = '0'; // quotient $r = $a; // remainder $z = $y; // focus length, always $y or $y+1 for (;;) { $focus = \substr($a, 0, $z); $cmp = $this->doCmp($focus, $b); if ($cmp === -1) { if ($z === $x) { // remainder < dividend break; } $z++; } $zeros = \str_repeat('0', $x - $z); $q = $this->add($q, '1' . $zeros); $a = $this->sub($a, $b . $zeros); $r = $a; if ($r === '0') { // remainder == 0 break; } $x = \strlen($a); if ($x < $y) { // remainder < dividend break; } $z = $y; } return [$q, $r]; } /** * Compares two non-signed large numbers. * * @param string $a The first operand. * @param string $b The second operand. * * @return int [-1, 0, 1] */ private function doCmp(string $a, string $b) : int { $x = \strlen($a); $y = \strlen($b); $cmp = $x <=> $y; if ($cmp !== 0) { return $cmp; } return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1] } /** * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length. * * The numbers must only consist of digits, without leading minus sign. * * @param string $a The first operand. * @param string $b The second operand. * * @return array{string, string, int} */ private function pad(string $a, string $b) : array { $x = \strlen($a); $y = \strlen($b); if ($x > $y) { $b = \str_repeat('0', $x - $y) . $b; return [$a, $b, $x]; } if ($x < $y) { $a = \str_repeat('0', $y - $x) . $a; return [$a, $b, $y]; } return [$a, $b, $x]; } } Internal/Calculator/GmpCalculator.php 0000644 00000005540 15025063510 0013660 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator; /** * Calculator implementation built around the GMP library. * * @internal * * @psalm-immutable */ class GmpCalculator extends Calculator { /** * {@inheritdoc} */ public function add(string $a, string $b) : string { return \gmp_strval(\gmp_add($a, $b)); } /** * {@inheritdoc} */ public function sub(string $a, string $b) : string { return \gmp_strval(\gmp_sub($a, $b)); } /** * {@inheritdoc} */ public function mul(string $a, string $b) : string { return \gmp_strval(\gmp_mul($a, $b)); } /** * {@inheritdoc} */ public function divQ(string $a, string $b) : string { return \gmp_strval(\gmp_div_q($a, $b)); } /** * {@inheritdoc} */ public function divR(string $a, string $b) : string { return \gmp_strval(\gmp_div_r($a, $b)); } /** * {@inheritdoc} */ public function divQR(string $a, string $b) : array { [$q, $r] = \gmp_div_qr($a, $b); return [ \gmp_strval($q), \gmp_strval($r) ]; } /** * {@inheritdoc} */ public function pow(string $a, int $e) : string { return \gmp_strval(\gmp_pow($a, $e)); } /** * {@inheritdoc} */ public function modInverse(string $x, string $m) : ?string { $result = \gmp_invert($x, $m); if ($result === false) { return null; } return \gmp_strval($result); } /** * {@inheritdoc} */ public function modPow(string $base, string $exp, string $mod) : string { return \gmp_strval(\gmp_powm($base, $exp, $mod)); } /** * {@inheritdoc} */ public function gcd(string $a, string $b) : string { return \gmp_strval(\gmp_gcd($a, $b)); } /** * {@inheritdoc} */ public function fromBase(string $number, int $base) : string { return \gmp_strval(\gmp_init($number, $base)); } /** * {@inheritdoc} */ public function toBase(string $number, int $base) : string { return \gmp_strval($number, $base); } /** * {@inheritdoc} */ public function and(string $a, string $b) : string { return \gmp_strval(\gmp_and($a, $b)); } /** * {@inheritdoc} */ public function or(string $a, string $b) : string { return \gmp_strval(\gmp_or($a, $b)); } /** * {@inheritdoc} */ public function xor(string $a, string $b) : string { return \gmp_strval(\gmp_xor($a, $b)); } /** * {@inheritDoc} */ public function sqrt(string $n) : string { return \gmp_strval(\gmp_sqrt($n)); } } Internal/Calculator/BcMathCalculator.php 0000644 00000004373 15025063510 0014276 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Internal\Calculator; use Brick\Math\Internal\Calculator; /** * Calculator implementation built around the bcmath library. * * @internal * * @psalm-immutable */ class BcMathCalculator extends Calculator { /** * {@inheritdoc} */ public function add(string $a, string $b) : string { return \bcadd($a, $b, 0); } /** * {@inheritdoc} */ public function sub(string $a, string $b) : string { return \bcsub($a, $b, 0); } /** * {@inheritdoc} */ public function mul(string $a, string $b) : string { return \bcmul($a, $b, 0); } /** * {@inheritdoc} * * @psalm-suppress InvalidNullableReturnType * @psalm-suppress NullableReturnStatement */ public function divQ(string $a, string $b) : string { return \bcdiv($a, $b, 0); } /** * {@inheritdoc} * * @psalm-suppress InvalidNullableReturnType * @psalm-suppress NullableReturnStatement */ public function divR(string $a, string $b) : string { if (version_compare(PHP_VERSION, '7.2') >= 0) { return \bcmod($a, $b, 0); } return \bcmod($a, $b); } /** * {@inheritdoc} */ public function divQR(string $a, string $b) : array { $q = \bcdiv($a, $b, 0); if (version_compare(PHP_VERSION, '7.2') >= 0) { $r = \bcmod($a, $b, 0); } else { $r = \bcmod($a, $b); } assert($q !== null); assert($r !== null); return [$q, $r]; } /** * {@inheritdoc} */ public function pow(string $a, int $e) : string { return \bcpow($a, (string) $e, 0); } /** * {@inheritdoc} * * @psalm-suppress InvalidNullableReturnType * @psalm-suppress NullableReturnStatement */ public function modPow(string $base, string $exp, string $mod) : string { return \bcpowmod($base, $exp, $mod, 0); } /** * {@inheritDoc} * * @psalm-suppress NullableReturnStatement * @psalm-suppress InvalidNullableReturnType */ public function sqrt(string $n) : string { return \bcsqrt($n, 0); } } Internal/Calculator.php 0000644 00000050727 15025063510 0011132 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Internal; use Brick\Math\Exception\RoundingNecessaryException; use Brick\Math\RoundingMode; /** * Performs basic operations on arbitrary size integers. * * Unless otherwise specified, all parameters must be validated as non-empty strings of digits, * without leading zero, and with an optional leading minus sign if the number is not zero. * * Any other parameter format will lead to undefined behaviour. * All methods must return strings respecting this format, unless specified otherwise. * * @internal * * @psalm-immutable */ abstract class Calculator { /** * The maximum exponent value allowed for the pow() method. */ public const MAX_POWER = 1000000; /** * The alphabet for converting from and to base 2 to 36, lowercase. */ public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; /** * The Calculator instance in use. * * @var Calculator|null */ private static $instance; /** * Sets the Calculator instance to use. * * An instance is typically set only in unit tests: the autodetect is usually the best option. * * @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect. * * @return void */ final public static function set(?Calculator $calculator) : void { self::$instance = $calculator; } /** * Returns the Calculator instance to use. * * If none has been explicitly set, the fastest available implementation will be returned. * * @return Calculator * * @psalm-pure * @psalm-suppress ImpureStaticProperty */ final public static function get() : Calculator { if (self::$instance === null) { /** @psalm-suppress ImpureMethodCall */ self::$instance = self::detect(); } return self::$instance; } /** * Returns the fastest available Calculator implementation. * * @codeCoverageIgnore * * @return Calculator */ private static function detect() : Calculator { if (\extension_loaded('gmp')) { return new Calculator\GmpCalculator(); } if (\extension_loaded('bcmath')) { return new Calculator\BcMathCalculator(); } return new Calculator\NativeCalculator(); } /** * Extracts the sign & digits of the operands. * * @param string $a The first operand. * @param string $b The second operand. * * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits. */ final protected function init(string $a, string $b) : array { return [ $aNeg = ($a[0] === '-'), $bNeg = ($b[0] === '-'), $aNeg ? \substr($a, 1) : $a, $bNeg ? \substr($b, 1) : $b, ]; } /** * Returns the absolute value of a number. * * @param string $n The number. * * @return string The absolute value. */ final public function abs(string $n) : string { return ($n[0] === '-') ? \substr($n, 1) : $n; } /** * Negates a number. * * @param string $n The number. * * @return string The negated value. */ final public function neg(string $n) : string { if ($n === '0') { return '0'; } if ($n[0] === '-') { return \substr($n, 1); } return '-' . $n; } /** * Compares two numbers. * * @param string $a The first number. * @param string $b The second number. * * @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number. */ final public function cmp(string $a, string $b) : int { [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); if ($aNeg && ! $bNeg) { return -1; } if ($bNeg && ! $aNeg) { return 1; } $aLen = \strlen($aDig); $bLen = \strlen($bDig); if ($aLen < $bLen) { $result = -1; } elseif ($aLen > $bLen) { $result = 1; } else { $result = $aDig <=> $bDig; } return $aNeg ? -$result : $result; } /** * Adds two numbers. * * @param string $a The augend. * @param string $b The addend. * * @return string The sum. */ abstract public function add(string $a, string $b) : string; /** * Subtracts two numbers. * * @param string $a The minuend. * @param string $b The subtrahend. * * @return string The difference. */ abstract public function sub(string $a, string $b) : string; /** * Multiplies two numbers. * * @param string $a The multiplicand. * @param string $b The multiplier. * * @return string The product. */ abstract public function mul(string $a, string $b) : string; /** * Returns the quotient of the division of two numbers. * * @param string $a The dividend. * @param string $b The divisor, must not be zero. * * @return string The quotient. */ abstract public function divQ(string $a, string $b) : string; /** * Returns the remainder of the division of two numbers. * * @param string $a The dividend. * @param string $b The divisor, must not be zero. * * @return string The remainder. */ abstract public function divR(string $a, string $b) : string; /** * Returns the quotient and remainder of the division of two numbers. * * @param string $a The dividend. * @param string $b The divisor, must not be zero. * * @return string[] An array containing the quotient and remainder. */ abstract public function divQR(string $a, string $b) : array; /** * Exponentiates a number. * * @param string $a The base number. * @param int $e The exponent, validated as an integer between 0 and MAX_POWER. * * @return string The power. */ abstract public function pow(string $a, int $e) : string; /** * @param string $a * @param string $b The modulus; must not be zero. * * @return string */ public function mod(string $a, string $b) : string { return $this->divR($this->add($this->divR($a, $b), $b), $b); } /** * Returns the modular multiplicative inverse of $x modulo $m. * * If $x has no multiplicative inverse mod m, this method must return null. * * This method can be overridden by the concrete implementation if the underlying library has built-in support. * * @param string $x * @param string $m The modulus; must not be negative or zero. * * @return string|null */ public function modInverse(string $x, string $m) : ?string { if ($m === '1') { return '0'; } $modVal = $x; if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) { $modVal = $this->mod($x, $m); } $x = '0'; $y = '0'; $g = $this->gcdExtended($modVal, $m, $x, $y); if ($g !== '1') { return null; } return $this->mod($this->add($this->mod($x, $m), $m), $m); } /** * Raises a number into power with modulo. * * @param string $base The base number; must be positive or zero. * @param string $exp The exponent; must be positive or zero. * @param string $mod The modulus; must be strictly positive. * * @return string The power. */ abstract public function modPow(string $base, string $exp, string $mod) : string; /** * Returns the greatest common divisor of the two numbers. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for GCD calculations. * * @param string $a The first number. * @param string $b The second number. * * @return string The GCD, always positive, or zero if both arguments are zero. */ public function gcd(string $a, string $b) : string { if ($a === '0') { return $this->abs($b); } if ($b === '0') { return $this->abs($a); } return $this->gcd($b, $this->divR($a, $b)); } private function gcdExtended(string $a, string $b, string &$x, string &$y) : string { if ($a === '0') { $x = '0'; $y = '1'; return $b; } $x1 = '0'; $y1 = '0'; $gcd = $this->gcdExtended($this->mod($b, $a), $a, $x1, $y1); $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1)); $y = $x1; return $gcd; } /** * Returns the square root of the given number, rounded down. * * The result is the largest x such that x² ≤ n. * The input MUST NOT be negative. * * @param string $n The number. * * @return string The square root. */ abstract public function sqrt(string $n) : string; /** * Converts a number from an arbitrary base. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for base conversion. * * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base. * @param int $base The base of the number, validated from 2 to 36. * * @return string The converted number, following the Calculator conventions. */ public function fromBase(string $number, int $base) : string { return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base); } /** * Converts a number to an arbitrary base. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for base conversion. * * @param string $number The number to convert, following the Calculator conventions. * @param int $base The base to convert to, validated from 2 to 36. * * @return string The converted number, lowercase. */ public function toBase(string $number, int $base) : string { $negative = ($number[0] === '-'); if ($negative) { $number = \substr($number, 1); } $number = $this->toArbitraryBase($number, self::ALPHABET, $base); if ($negative) { return '-' . $number; } return $number; } /** * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10. * * @param string $number The number to convert, validated as a non-empty string, * containing only chars in the given alphabet/base. * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. * @param int $base The base of the number, validated from 2 to alphabet length. * * @return string The number in base 10, following the Calculator conventions. */ final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string { // remove leading "zeros" $number = \ltrim($number, $alphabet[0]); if ($number === '') { return '0'; } // optimize for "one" if ($number === $alphabet[1]) { return '1'; } $result = '0'; $power = '1'; $base = (string) $base; for ($i = \strlen($number) - 1; $i >= 0; $i--) { $index = \strpos($alphabet, $number[$i]); if ($index !== 0) { $result = $this->add($result, ($index === 1) ? $power : $this->mul($power, (string) $index) ); } if ($i !== 0) { $power = $this->mul($power, $base); } } return $result; } /** * Converts a non-negative number to an arbitrary base using a custom alphabet. * * @param string $number The number to convert, positive or zero, following the Calculator conventions. * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. * @param int $base The base to convert to, validated from 2 to alphabet length. * * @return string The converted number in the given alphabet. */ final public function toArbitraryBase(string $number, string $alphabet, int $base) : string { if ($number === '0') { return $alphabet[0]; } $base = (string) $base; $result = ''; while ($number !== '0') { [$number, $remainder] = $this->divQR($number, $base); $remainder = (int) $remainder; $result .= $alphabet[$remainder]; } return \strrev($result); } /** * Performs a rounded division. * * Rounding is performed when the remainder of the division is not zero. * * @param string $a The dividend. * @param string $b The divisor, must not be zero. * @param int $roundingMode The rounding mode. * * @return string * * @throws \InvalidArgumentException If the rounding mode is invalid. * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary. */ final public function divRound(string $a, string $b, int $roundingMode) : string { [$quotient, $remainder] = $this->divQR($a, $b); $hasDiscardedFraction = ($remainder !== '0'); $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-'); $discardedFractionSign = function() use ($remainder, $b) : int { $r = $this->abs($this->mul($remainder, '2')); $b = $this->abs($b); return $this->cmp($r, $b); }; $increment = false; switch ($roundingMode) { case RoundingMode::UNNECESSARY: if ($hasDiscardedFraction) { throw RoundingNecessaryException::roundingNecessary(); } break; case RoundingMode::UP: $increment = $hasDiscardedFraction; break; case RoundingMode::DOWN: break; case RoundingMode::CEILING: $increment = $hasDiscardedFraction && $isPositiveOrZero; break; case RoundingMode::FLOOR: $increment = $hasDiscardedFraction && ! $isPositiveOrZero; break; case RoundingMode::HALF_UP: $increment = $discardedFractionSign() >= 0; break; case RoundingMode::HALF_DOWN: $increment = $discardedFractionSign() > 0; break; case RoundingMode::HALF_CEILING: $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0; break; case RoundingMode::HALF_FLOOR: $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; break; case RoundingMode::HALF_EVEN: $lastDigit = (int) $quotient[-1]; $lastDigitIsEven = ($lastDigit % 2 === 0); $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; break; default: throw new \InvalidArgumentException('Invalid rounding mode.'); } if ($increment) { return $this->add($quotient, $isPositiveOrZero ? '1' : '-1'); } return $quotient; } /** * Calculates bitwise AND of two numbers. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for bitwise operations. * * @param string $a * @param string $b * * @return string */ public function and(string $a, string $b) : string { return $this->bitwise('and', $a, $b); } /** * Calculates bitwise OR of two numbers. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for bitwise operations. * * @param string $a * @param string $b * * @return string */ public function or(string $a, string $b) : string { return $this->bitwise('or', $a, $b); } /** * Calculates bitwise XOR of two numbers. * * This method can be overridden by the concrete implementation if the underlying library * has built-in support for bitwise operations. * * @param string $a * @param string $b * * @return string */ public function xor(string $a, string $b) : string { return $this->bitwise('xor', $a, $b); } /** * Performs a bitwise operation on a decimal number. * * @param string $operator The operator to use, must be "and", "or" or "xor". * @param string $a The left operand. * @param string $b The right operand. * * @return string */ private function bitwise(string $operator, string $a, string $b) : string { [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); $aBin = $this->toBinary($aDig); $bBin = $this->toBinary($bDig); $aLen = \strlen($aBin); $bLen = \strlen($bBin); if ($aLen > $bLen) { $bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin; } elseif ($bLen > $aLen) { $aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin; } if ($aNeg) { $aBin = $this->twosComplement($aBin); } if ($bNeg) { $bBin = $this->twosComplement($bBin); } switch ($operator) { case 'and': $value = $aBin & $bBin; $negative = ($aNeg and $bNeg); break; case 'or': $value = $aBin | $bBin; $negative = ($aNeg or $bNeg); break; case 'xor': $value = $aBin ^ $bBin; $negative = ($aNeg xor $bNeg); break; // @codeCoverageIgnoreStart default: throw new \InvalidArgumentException('Invalid bitwise operator.'); // @codeCoverageIgnoreEnd } if ($negative) { $value = $this->twosComplement($value); } $result = $this->toDecimal($value); return $negative ? $this->neg($result) : $result; } /** * @param string $number A positive, binary number. * * @return string */ private function twosComplement(string $number) : string { $xor = \str_repeat("\xff", \strlen($number)); $number ^= $xor; for ($i = \strlen($number) - 1; $i >= 0; $i--) { $byte = \ord($number[$i]); if (++$byte !== 256) { $number[$i] = \chr($byte); break; } $number[$i] = "\x00"; if ($i === 0) { $number = "\x01" . $number; } } return $number; } /** * Converts a decimal number to a binary string. * * @param string $number The number to convert, positive or zero, only digits. * * @return string */ private function toBinary(string $number) : string { $result = ''; while ($number !== '0') { [$number, $remainder] = $this->divQR($number, '256'); $result .= \chr((int) $remainder); } return \strrev($result); } /** * Returns the positive decimal representation of a binary number. * * @param string $bytes The bytes representing the number. * * @return string */ private function toDecimal(string $bytes) : string { $result = '0'; $power = '1'; for ($i = \strlen($bytes) - 1; $i >= 0; $i--) { $index = \ord($bytes[$i]); if ($index !== 0) { $result = $this->add($result, ($index === 1) ? $power : $this->mul($power, (string) $index) ); } if ($i !== 0) { $power = $this->mul($power, '256'); } } return $result; } } BigDecimal.php 0000644 00000056404 15025063510 0007243 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math; use Brick\Math\Exception\DivisionByZeroException; use Brick\Math\Exception\MathException; use Brick\Math\Exception\NegativeNumberException; use Brick\Math\Internal\Calculator; /** * Immutable, arbitrary-precision signed decimal numbers. * * @psalm-immutable */ final class BigDecimal extends BigNumber { /** * The unscaled value of this decimal number. * * This is a string of digits with an optional leading minus sign. * No leading zero must be present. * No leading minus sign must be present if the value is 0. * * @var string */ private $value; /** * The scale (number of digits after the decimal point) of this decimal number. * * This must be zero or more. * * @var int */ private $scale; /** * Protected constructor. Use a factory method to obtain an instance. * * @param string $value The unscaled value, validated. * @param int $scale The scale, validated. */ protected function __construct(string $value, int $scale = 0) { $this->value = $value; $this->scale = $scale; } /** * Creates a BigDecimal of the given value. * * @param BigNumber|int|float|string $value * * @return BigDecimal * * @throws MathException If the value cannot be converted to a BigDecimal. * * @psalm-pure */ public static function of($value) : BigNumber { return parent::of($value)->toBigDecimal(); } /** * Creates a BigDecimal from an unscaled value and a scale. * * Example: `(12345, 3)` will result in the BigDecimal `12.345`. * * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger. * @param int $scale The scale of the number, positive or zero. * * @return BigDecimal * * @throws \InvalidArgumentException If the scale is negative. * * @psalm-pure */ public static function ofUnscaledValue($value, int $scale = 0) : BigDecimal { if ($scale < 0) { throw new \InvalidArgumentException('The scale cannot be negative.'); } return new BigDecimal((string) BigInteger::of($value), $scale); } /** * Returns a BigDecimal representing zero, with a scale of zero. * * @return BigDecimal * * @psalm-pure */ public static function zero() : BigDecimal { /** * @psalm-suppress ImpureStaticVariable * @var BigDecimal|null $zero */ static $zero; if ($zero === null) { $zero = new BigDecimal('0'); } return $zero; } /** * Returns a BigDecimal representing one, with a scale of zero. * * @return BigDecimal * * @psalm-pure */ public static function one() : BigDecimal { /** * @psalm-suppress ImpureStaticVariable * @var BigDecimal|null $one */ static $one; if ($one === null) { $one = new BigDecimal('1'); } return $one; } /** * Returns a BigDecimal representing ten, with a scale of zero. * * @return BigDecimal * * @psalm-pure */ public static function ten() : BigDecimal { /** * @psalm-suppress ImpureStaticVariable * @var BigDecimal|null $ten */ static $ten; if ($ten === null) { $ten = new BigDecimal('10'); } return $ten; } /** * Returns the sum of this number and the given one. * * The result has a scale of `max($this->scale, $that->scale)`. * * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal. * * @return BigDecimal The result. * * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. */ public function plus($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->value === '0' && $that->scale <= $this->scale) { return $this; } if ($this->value === '0' && $this->scale <= $that->scale) { return $that; } [$a, $b] = $this->scaleValues($this, $that); $value = Calculator::get()->add($a, $b); $scale = $this->scale > $that->scale ? $this->scale : $that->scale; return new BigDecimal($value, $scale); } /** * Returns the difference of this number and the given one. * * The result has a scale of `max($this->scale, $that->scale)`. * * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal. * * @return BigDecimal The result. * * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. */ public function minus($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->value === '0' && $that->scale <= $this->scale) { return $this; } [$a, $b] = $this->scaleValues($this, $that); $value = Calculator::get()->sub($a, $b); $scale = $this->scale > $that->scale ? $this->scale : $that->scale; return new BigDecimal($value, $scale); } /** * Returns the product of this number and the given one. * * The result has a scale of `$this->scale + $that->scale`. * * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal. * * @return BigDecimal The result. * * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. */ public function multipliedBy($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->value === '1' && $that->scale === 0) { return $this; } if ($this->value === '1' && $this->scale === 0) { return $that; } $value = Calculator::get()->mul($this->value, $that->value); $scale = $this->scale + $that->scale; return new BigDecimal($value, $scale); } /** * Returns the result of the division of this number by the given one, at the given scale. * * @param BigNumber|int|float|string $that The divisor. * @param int|null $scale The desired scale, or null to use the scale of this number. * @param int $roundingMode An optional rounding mode. * * @return BigDecimal * * @throws \InvalidArgumentException If the scale or rounding mode is invalid. * @throws MathException If the number is invalid, is zero, or rounding was necessary. */ public function dividedBy($that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { $that = BigDecimal::of($that); if ($that->isZero()) { throw DivisionByZeroException::divisionByZero(); } if ($scale === null) { $scale = $this->scale; } elseif ($scale < 0) { throw new \InvalidArgumentException('Scale cannot be negative.'); } if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) { return $this; } $p = $this->valueWithMinScale($that->scale + $scale); $q = $that->valueWithMinScale($this->scale - $scale); $result = Calculator::get()->divRound($p, $q, $roundingMode); return new BigDecimal($result, $scale); } /** * Returns the exact result of the division of this number by the given one. * * The scale of the result is automatically calculated to fit all the fraction digits. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * * @return BigDecimal The result. * * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero, * or the result yields an infinite number of digits. */ public function exactlyDividedBy($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->value === '0') { throw DivisionByZeroException::divisionByZero(); } [, $b] = $this->scaleValues($this, $that); $d = \rtrim($b, '0'); $scale = \strlen($b) - \strlen($d); $calculator = Calculator::get(); foreach ([5, 2] as $prime) { for (;;) { $lastDigit = (int) $d[-1]; if ($lastDigit % $prime !== 0) { break; } $d = $calculator->divQ($d, (string) $prime); $scale++; } } return $this->dividedBy($that, $scale)->stripTrailingZeros(); } /** * Returns this number exponentiated to the given value. * * The result has a scale of `$this->scale * $exponent`. * * @param int $exponent The exponent. * * @return BigDecimal The result. * * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. */ public function power(int $exponent) : BigDecimal { if ($exponent === 0) { return BigDecimal::one(); } if ($exponent === 1) { return $this; } if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { throw new \InvalidArgumentException(\sprintf( 'The exponent %d is not in the range 0 to %d.', $exponent, Calculator::MAX_POWER )); } return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent); } /** * Returns the quotient of the division of this number by this given one. * * The quotient has a scale of `0`. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * * @return BigDecimal The quotient. * * @throws MathException If the divisor is not a valid decimal number, or is zero. */ public function quotient($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->isZero()) { throw DivisionByZeroException::divisionByZero(); } $p = $this->valueWithMinScale($that->scale); $q = $that->valueWithMinScale($this->scale); $quotient = Calculator::get()->divQ($p, $q); return new BigDecimal($quotient, 0); } /** * Returns the remainder of the division of this number by this given one. * * The remainder has a scale of `max($this->scale, $that->scale)`. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * * @return BigDecimal The remainder. * * @throws MathException If the divisor is not a valid decimal number, or is zero. */ public function remainder($that) : BigDecimal { $that = BigDecimal::of($that); if ($that->isZero()) { throw DivisionByZeroException::divisionByZero(); } $p = $this->valueWithMinScale($that->scale); $q = $that->valueWithMinScale($this->scale); $remainder = Calculator::get()->divR($p, $q); $scale = $this->scale > $that->scale ? $this->scale : $that->scale; return new BigDecimal($remainder, $scale); } /** * Returns the quotient and remainder of the division of this number by the given one. * * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. * * @return BigDecimal[] An array containing the quotient and the remainder. * * @throws MathException If the divisor is not a valid decimal number, or is zero. */ public function quotientAndRemainder($that) : array { $that = BigDecimal::of($that); if ($that->isZero()) { throw DivisionByZeroException::divisionByZero(); } $p = $this->valueWithMinScale($that->scale); $q = $that->valueWithMinScale($this->scale); [$quotient, $remainder] = Calculator::get()->divQR($p, $q); $scale = $this->scale > $that->scale ? $this->scale : $that->scale; $quotient = new BigDecimal($quotient, 0); $remainder = new BigDecimal($remainder, $scale); return [$quotient, $remainder]; } /** * Returns the square root of this number, rounded down to the given number of decimals. * * @param int $scale * * @return BigDecimal * * @throws \InvalidArgumentException If the scale is negative. * @throws NegativeNumberException If this number is negative. */ public function sqrt(int $scale) : BigDecimal { if ($scale < 0) { throw new \InvalidArgumentException('Scale cannot be negative.'); } if ($this->value === '0') { return new BigDecimal('0', $scale); } if ($this->value[0] === '-') { throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); } $value = $this->value; $addDigits = 2 * $scale - $this->scale; if ($addDigits > 0) { // add zeros $value .= \str_repeat('0', $addDigits); } elseif ($addDigits < 0) { // trim digits if (-$addDigits >= \strlen($this->value)) { // requesting a scale too low, will always yield a zero result return new BigDecimal('0', $scale); } $value = \substr($value, 0, $addDigits); } $value = Calculator::get()->sqrt($value); return new BigDecimal($value, $scale); } /** * Returns a copy of this BigDecimal with the decimal point moved $n places to the left. * * @param int $n * * @return BigDecimal */ public function withPointMovedLeft(int $n) : BigDecimal { if ($n === 0) { return $this; } if ($n < 0) { return $this->withPointMovedRight(-$n); } return new BigDecimal($this->value, $this->scale + $n); } /** * Returns a copy of this BigDecimal with the decimal point moved $n places to the right. * * @param int $n * * @return BigDecimal */ public function withPointMovedRight(int $n) : BigDecimal { if ($n === 0) { return $this; } if ($n < 0) { return $this->withPointMovedLeft(-$n); } $value = $this->value; $scale = $this->scale - $n; if ($scale < 0) { if ($value !== '0') { $value .= \str_repeat('0', -$scale); } $scale = 0; } return new BigDecimal($value, $scale); } /** * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part. * * @return BigDecimal */ public function stripTrailingZeros() : BigDecimal { if ($this->scale === 0) { return $this; } $trimmedValue = \rtrim($this->value, '0'); if ($trimmedValue === '') { return BigDecimal::zero(); } $trimmableZeros = \strlen($this->value) - \strlen($trimmedValue); if ($trimmableZeros === 0) { return $this; } if ($trimmableZeros > $this->scale) { $trimmableZeros = $this->scale; } $value = \substr($this->value, 0, -$trimmableZeros); $scale = $this->scale - $trimmableZeros; return new BigDecimal($value, $scale); } /** * Returns the absolute value of this number. * * @return BigDecimal */ public function abs() : BigDecimal { return $this->isNegative() ? $this->negated() : $this; } /** * Returns the negated value of this number. * * @return BigDecimal */ public function negated() : BigDecimal { return new BigDecimal(Calculator::get()->neg($this->value), $this->scale); } /** * {@inheritdoc} */ public function compareTo($that) : int { $that = BigNumber::of($that); if ($that instanceof BigInteger) { $that = $that->toBigDecimal(); } if ($that instanceof BigDecimal) { [$a, $b] = $this->scaleValues($this, $that); return Calculator::get()->cmp($a, $b); } return - $that->compareTo($this); } /** * {@inheritdoc} */ public function getSign() : int { return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); } /** * @return BigInteger */ public function getUnscaledValue() : BigInteger { return BigInteger::create($this->value); } /** * @return int */ public function getScale() : int { return $this->scale; } /** * Returns a string representing the integral part of this decimal number. * * Example: `-123.456` => `-123`. * * @return string */ public function getIntegralPart() : string { if ($this->scale === 0) { return $this->value; } $value = $this->getUnscaledValueWithLeadingZeros(); return \substr($value, 0, -$this->scale); } /** * Returns a string representing the fractional part of this decimal number. * * If the scale is zero, an empty string is returned. * * Examples: `-123.456` => '456', `123` => ''. * * @return string */ public function getFractionalPart() : string { if ($this->scale === 0) { return ''; } $value = $this->getUnscaledValueWithLeadingZeros(); return \substr($value, -$this->scale); } /** * Returns whether this decimal number has a non-zero fractional part. * * @return bool */ public function hasNonZeroFractionalPart() : bool { return $this->getFractionalPart() !== \str_repeat('0', $this->scale); } /** * {@inheritdoc} */ public function toBigInteger() : BigInteger { $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); return BigInteger::create($zeroScaleDecimal->value); } /** * {@inheritdoc} */ public function toBigDecimal() : BigDecimal { return $this; } /** * {@inheritdoc} */ public function toBigRational() : BigRational { $numerator = BigInteger::create($this->value); $denominator = BigInteger::create('1' . \str_repeat('0', $this->scale)); return BigRational::create($numerator, $denominator, false); } /** * {@inheritdoc} */ public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { if ($scale === $this->scale) { return $this; } return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode); } /** * {@inheritdoc} */ public function toInt() : int { return $this->toBigInteger()->toInt(); } /** * {@inheritdoc} */ public function toFloat() : float { return (float) (string) $this; } /** * {@inheritdoc} */ public function __toString() : string { if ($this->scale === 0) { return $this->value; } $value = $this->getUnscaledValueWithLeadingZeros(); return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale); } /** * This method is required for serializing the object and SHOULD NOT be accessed directly. * * @internal * * @return array{value: string, scale: int} */ public function __serialize(): array { return ['value' => $this->value, 'scale' => $this->scale]; } /** * This method is only here to allow unserializing the object and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param array{value: string, scale: int} $data * * @return void * * @throws \LogicException */ public function __unserialize(array $data): void { if (isset($this->value)) { throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); } $this->value = $data['value']; $this->scale = $data['scale']; } /** * This method is required by interface Serializable and SHOULD NOT be accessed directly. * * @internal * * @return string */ public function serialize() : string { return $this->value . ':' . $this->scale; } /** * This method is only here to implement interface Serializable and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param string $value * * @return void * * @throws \LogicException */ public function unserialize($value) : void { if (isset($this->value)) { throw new \LogicException('unserialize() is an internal function, it must not be called directly.'); } [$value, $scale] = \explode(':', $value); $this->value = $value; $this->scale = (int) $scale; } /** * Puts the internal values of the given decimal numbers on the same scale. * * @param BigDecimal $x The first decimal number. * @param BigDecimal $y The second decimal number. * * @return array{string, string} The scaled integer values of $x and $y. */ private function scaleValues(BigDecimal $x, BigDecimal $y) : array { $a = $x->value; $b = $y->value; if ($b !== '0' && $x->scale > $y->scale) { $b .= \str_repeat('0', $x->scale - $y->scale); } elseif ($a !== '0' && $x->scale < $y->scale) { $a .= \str_repeat('0', $y->scale - $x->scale); } return [$a, $b]; } /** * @param int $scale * * @return string */ private function valueWithMinScale(int $scale) : string { $value = $this->value; if ($this->value !== '0' && $scale > $this->scale) { $value .= \str_repeat('0', $scale - $this->scale); } return $value; } /** * Adds leading zeros if necessary to the unscaled value to represent the full decimal number. * * @return string */ private function getUnscaledValueWithLeadingZeros() : string { $value = $this->value; $targetLength = $this->scale + 1; $negative = ($value[0] === '-'); $length = \strlen($value); if ($negative) { $length--; } if ($length >= $targetLength) { return $this->value; } if ($negative) { $value = \substr($value, 1); } $value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT); if ($negative) { $value = '-' . $value; } return $value; } } BigNumber.php 0000644 00000040170 15025063510 0007126 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math; use Brick\Math\Exception\DivisionByZeroException; use Brick\Math\Exception\MathException; use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\RoundingNecessaryException; /** * Common interface for arbitrary-precision rational numbers. * * @psalm-immutable */ abstract class BigNumber implements \Serializable, \JsonSerializable { /** * The regular expression used to parse integer, decimal and rational numbers. */ private const PARSE_REGEXP = '/^' . '(?<sign>[\-\+])?' . '(?:' . '(?:' . '(?<integral>[0-9]+)?' . '(?<point>\.)?' . '(?<fractional>[0-9]+)?' . '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' . ')|(?:' . '(?<numerator>[0-9]+)' . '\/?' . '(?<denominator>[0-9]+)' . ')' . ')' . '$/'; /** * Creates a BigNumber of the given value. * * The concrete return type is dependent on the given value, with the following rules: * * - BigNumber instances are returned as is * - integer numbers are returned as BigInteger * - floating point numbers are converted to a string then parsed as such * - strings containing a `/` character are returned as BigRational * - strings containing a `.` character or using an exponential notation are returned as BigDecimal * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger * * @param BigNumber|int|float|string $value * * @return BigNumber * * @throws NumberFormatException If the format of the number is not valid. * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. * * @psalm-pure */ public static function of($value) : BigNumber { if ($value instanceof BigNumber) { return $value; } if (\is_int($value)) { return new BigInteger((string) $value); } /** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */ $value = \is_float($value) ? self::floatToString($value) : (string) $value; $throw = static function() use ($value) : void { throw new NumberFormatException(\sprintf( 'The given value "%s" does not represent a valid number.', $value )); }; if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) { $throw(); } $getMatch = static function(string $value) use ($matches) : ?string { return isset($matches[$value]) && $matches[$value] !== '' ? $matches[$value] : null; }; $sign = $getMatch('sign'); $numerator = $getMatch('numerator'); $denominator = $getMatch('denominator'); if ($numerator !== null) { assert($denominator !== null); if ($sign !== null) { $numerator = $sign . $numerator; } $numerator = self::cleanUp($numerator); $denominator = self::cleanUp($denominator); if ($denominator === '0') { throw DivisionByZeroException::denominatorMustNotBeZero(); } return new BigRational( new BigInteger($numerator), new BigInteger($denominator), false ); } $point = $getMatch('point'); $integral = $getMatch('integral'); $fractional = $getMatch('fractional'); $exponent = $getMatch('exponent'); if ($integral === null && $fractional === null) { $throw(); } if ($integral === null) { $integral = '0'; } if ($point !== null || $exponent !== null) { $fractional = ($fractional ?? ''); $exponent = ($exponent !== null) ? (int) $exponent : 0; if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { throw new NumberFormatException('Exponent too large.'); } $unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional); $scale = \strlen($fractional) - $exponent; if ($scale < 0) { if ($unscaledValue !== '0') { $unscaledValue .= \str_repeat('0', - $scale); } $scale = 0; } return new BigDecimal($unscaledValue, $scale); } $integral = self::cleanUp(($sign ?? '') . $integral); return new BigInteger($integral); } /** * Safely converts float to string, avoiding locale-dependent issues. * * @see https://github.com/brick/math/pull/20 * * @param float $float * * @return string * * @psalm-pure * @psalm-suppress ImpureFunctionCall */ private static function floatToString(float $float) : string { $currentLocale = \setlocale(LC_NUMERIC, '0'); \setlocale(LC_NUMERIC, 'C'); $result = (string) $float; \setlocale(LC_NUMERIC, $currentLocale); return $result; } /** * Proxy method to access protected constructors from sibling classes. * * @internal * * @param mixed ...$args The arguments to the constructor. * * @return static * * @psalm-pure * @psalm-suppress TooManyArguments * @psalm-suppress UnsafeInstantiation */ protected static function create(... $args) : BigNumber { return new static(... $args); } /** * Returns the minimum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The minimum value. * * @throws \InvalidArgumentException If no values are given. * @throws MathException If an argument is not valid. * * @psalm-suppress LessSpecificReturnStatement * @psalm-suppress MoreSpecificReturnType * @psalm-pure */ public static function min(...$values) : BigNumber { $min = null; foreach ($values as $value) { $value = static::of($value); if ($min === null || $value->isLessThan($min)) { $min = $value; } } if ($min === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $min; } /** * Returns the maximum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The maximum value. * * @throws \InvalidArgumentException If no values are given. * @throws MathException If an argument is not valid. * * @psalm-suppress LessSpecificReturnStatement * @psalm-suppress MoreSpecificReturnType * @psalm-pure */ public static function max(...$values) : BigNumber { $max = null; foreach ($values as $value) { $value = static::of($value); if ($max === null || $value->isGreaterThan($max)) { $max = $value; } } if ($max === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $max; } /** * Returns the sum of the given values. * * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible * to an instance of the class this method is called on. * * @return static The sum. * * @throws \InvalidArgumentException If no values are given. * @throws MathException If an argument is not valid. * * @psalm-suppress LessSpecificReturnStatement * @psalm-suppress MoreSpecificReturnType * @psalm-pure */ public static function sum(...$values) : BigNumber { /** @var BigNumber|null $sum */ $sum = null; foreach ($values as $value) { $value = static::of($value); $sum = $sum === null ? $value : self::add($sum, $value); } if ($sum === null) { throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); } return $sum; } /** * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. * * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to * concrete classes the responsibility to perform the addition themselves or delegate it to the given number, * depending on their ability to perform the operation. This will also require a version bump because we're * potentially breaking custom BigNumber implementations (if any...) * * @param BigNumber $a * @param BigNumber $b * * @return BigNumber * * @psalm-pure */ private static function add(BigNumber $a, BigNumber $b) : BigNumber { if ($a instanceof BigRational) { return $a->plus($b); } if ($b instanceof BigRational) { return $b->plus($a); } if ($a instanceof BigDecimal) { return $a->plus($b); } if ($b instanceof BigDecimal) { return $b->plus($a); } /** @var BigInteger $a */ return $a->plus($b); } /** * Removes optional leading zeros and + sign from the given number. * * @param string $number The number, validated as a non-empty string of digits with optional leading sign. * * @return string * * @psalm-pure */ private static function cleanUp(string $number) : string { $firstChar = $number[0]; if ($firstChar === '+' || $firstChar === '-') { $number = \substr($number, 1); } $number = \ltrim($number, '0'); if ($number === '') { return '0'; } if ($firstChar === '-') { return '-' . $number; } return $number; } /** * Checks if this number is equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isEqualTo($that) : bool { return $this->compareTo($that) === 0; } /** * Checks if this number is strictly lower than the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isLessThan($that) : bool { return $this->compareTo($that) < 0; } /** * Checks if this number is lower than or equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isLessThanOrEqualTo($that) : bool { return $this->compareTo($that) <= 0; } /** * Checks if this number is strictly greater than the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isGreaterThan($that) : bool { return $this->compareTo($that) > 0; } /** * Checks if this number is greater than or equal to the given one. * * @param BigNumber|int|float|string $that * * @return bool */ public function isGreaterThanOrEqualTo($that) : bool { return $this->compareTo($that) >= 0; } /** * Checks if this number equals zero. * * @return bool */ public function isZero() : bool { return $this->getSign() === 0; } /** * Checks if this number is strictly negative. * * @return bool */ public function isNegative() : bool { return $this->getSign() < 0; } /** * Checks if this number is negative or zero. * * @return bool */ public function isNegativeOrZero() : bool { return $this->getSign() <= 0; } /** * Checks if this number is strictly positive. * * @return bool */ public function isPositive() : bool { return $this->getSign() > 0; } /** * Checks if this number is positive or zero. * * @return bool */ public function isPositiveOrZero() : bool { return $this->getSign() >= 0; } /** * Returns the sign of this number. * * @return int -1 if the number is negative, 0 if zero, 1 if positive. */ abstract public function getSign() : int; /** * Compares this number to the given one. * * @param BigNumber|int|float|string $that * * @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`. * * @throws MathException If the number is not valid. */ abstract public function compareTo($that) : int; /** * Converts this number to a BigInteger. * * @return BigInteger The converted number. * * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. */ abstract public function toBigInteger() : BigInteger; /** * Converts this number to a BigDecimal. * * @return BigDecimal The converted number. * * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. */ abstract public function toBigDecimal() : BigDecimal; /** * Converts this number to a BigRational. * * @return BigRational The converted number. */ abstract public function toBigRational() : BigRational; /** * Converts this number to a BigDecimal with the given scale, using rounding if necessary. * * @param int $scale The scale of the resulting `BigDecimal`. * @param int $roundingMode A `RoundingMode` constant. * * @return BigDecimal * * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding. * This only applies when RoundingMode::UNNECESSARY is used. */ abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal; /** * Returns the exact value of this number as a native integer. * * If this number cannot be converted to a native integer without losing precision, an exception is thrown. * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit. * * @return int The converted value. * * @throws MathException If this number cannot be exactly converted to a native integer. */ abstract public function toInt() : int; /** * Returns an approximation of this number as a floating-point value. * * Note that this method can discard information as the precision of a floating-point value * is inherently limited. * * If the number is greater than the largest representable floating point number, positive infinity is returned. * If the number is less than the smallest representable floating point number, negative infinity is returned. * * @return float The converted value. */ abstract public function toFloat() : float; /** * Returns a string representation of this number. * * The output of this method can be parsed by the `of()` factory method; * this will yield an object equal to this one, without any information loss. * * @return string */ abstract public function __toString() : string; /** * {@inheritdoc} */ public function jsonSerialize() : string { return $this->__toString(); } } BigRational.php 0000644 00000032750 15025063510 0007454 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math; use Brick\Math\Exception\DivisionByZeroException; use Brick\Math\Exception\MathException; use Brick\Math\Exception\NumberFormatException; use Brick\Math\Exception\RoundingNecessaryException; /** * An arbitrarily large rational number. * * This class is immutable. * * @psalm-immutable */ final class BigRational extends BigNumber { /** * The numerator. * * @var BigInteger */ private $numerator; /** * The denominator. Always strictly positive. * * @var BigInteger */ private $denominator; /** * Protected constructor. Use a factory method to obtain an instance. * * @param BigInteger $numerator The numerator. * @param BigInteger $denominator The denominator. * @param bool $checkDenominator Whether to check the denominator for negative and zero. * * @throws DivisionByZeroException If the denominator is zero. */ protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) { if ($checkDenominator) { if ($denominator->isZero()) { throw DivisionByZeroException::denominatorMustNotBeZero(); } if ($denominator->isNegative()) { $numerator = $numerator->negated(); $denominator = $denominator->negated(); } } $this->numerator = $numerator; $this->denominator = $denominator; } /** * Creates a BigRational of the given value. * * @param BigNumber|int|float|string $value * * @return BigRational * * @throws MathException If the value cannot be converted to a BigRational. * * @psalm-pure */ public static function of($value) : BigNumber { return parent::of($value)->toBigRational(); } /** * Creates a BigRational out of a numerator and a denominator. * * If the denominator is negative, the signs of both the numerator and the denominator * will be inverted to ensure that the denominator is always positive. * * @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger. * * @return BigRational * * @throws NumberFormatException If an argument does not represent a valid number. * @throws RoundingNecessaryException If an argument represents a non-integer number. * @throws DivisionByZeroException If the denominator is zero. * * @psalm-pure */ public static function nd($numerator, $denominator) : BigRational { $numerator = BigInteger::of($numerator); $denominator = BigInteger::of($denominator); return new BigRational($numerator, $denominator, true); } /** * Returns a BigRational representing zero. * * @return BigRational * * @psalm-pure */ public static function zero() : BigRational { /** * @psalm-suppress ImpureStaticVariable * @var BigRational|null $zero */ static $zero; if ($zero === null) { $zero = new BigRational(BigInteger::zero(), BigInteger::one(), false); } return $zero; } /** * Returns a BigRational representing one. * * @return BigRational * * @psalm-pure */ public static function one() : BigRational { /** * @psalm-suppress ImpureStaticVariable * @var BigRational|null $one */ static $one; if ($one === null) { $one = new BigRational(BigInteger::one(), BigInteger::one(), false); } return $one; } /** * Returns a BigRational representing ten. * * @return BigRational * * @psalm-pure */ public static function ten() : BigRational { /** * @psalm-suppress ImpureStaticVariable * @var BigRational|null $ten */ static $ten; if ($ten === null) { $ten = new BigRational(BigInteger::ten(), BigInteger::one(), false); } return $ten; } /** * @return BigInteger */ public function getNumerator() : BigInteger { return $this->numerator; } /** * @return BigInteger */ public function getDenominator() : BigInteger { return $this->denominator; } /** * Returns the quotient of the division of the numerator by the denominator. * * @return BigInteger */ public function quotient() : BigInteger { return $this->numerator->quotient($this->denominator); } /** * Returns the remainder of the division of the numerator by the denominator. * * @return BigInteger */ public function remainder() : BigInteger { return $this->numerator->remainder($this->denominator); } /** * Returns the quotient and remainder of the division of the numerator by the denominator. * * @return BigInteger[] */ public function quotientAndRemainder() : array { return $this->numerator->quotientAndRemainder($this->denominator); } /** * Returns the sum of this number and the given one. * * @param BigNumber|int|float|string $that The number to add. * * @return BigRational The result. * * @throws MathException If the number is not valid. */ public function plus($that) : BigRational { $that = BigRational::of($that); $numerator = $this->numerator->multipliedBy($that->denominator); $numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator)); $denominator = $this->denominator->multipliedBy($that->denominator); return new BigRational($numerator, $denominator, false); } /** * Returns the difference of this number and the given one. * * @param BigNumber|int|float|string $that The number to subtract. * * @return BigRational The result. * * @throws MathException If the number is not valid. */ public function minus($that) : BigRational { $that = BigRational::of($that); $numerator = $this->numerator->multipliedBy($that->denominator); $numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator)); $denominator = $this->denominator->multipliedBy($that->denominator); return new BigRational($numerator, $denominator, false); } /** * Returns the product of this number and the given one. * * @param BigNumber|int|float|string $that The multiplier. * * @return BigRational The result. * * @throws MathException If the multiplier is not a valid number. */ public function multipliedBy($that) : BigRational { $that = BigRational::of($that); $numerator = $this->numerator->multipliedBy($that->numerator); $denominator = $this->denominator->multipliedBy($that->denominator); return new BigRational($numerator, $denominator, false); } /** * Returns the result of the division of this number by the given one. * * @param BigNumber|int|float|string $that The divisor. * * @return BigRational The result. * * @throws MathException If the divisor is not a valid number, or is zero. */ public function dividedBy($that) : BigRational { $that = BigRational::of($that); $numerator = $this->numerator->multipliedBy($that->denominator); $denominator = $this->denominator->multipliedBy($that->numerator); return new BigRational($numerator, $denominator, true); } /** * Returns this number exponentiated to the given value. * * @param int $exponent The exponent. * * @return BigRational The result. * * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. */ public function power(int $exponent) : BigRational { if ($exponent === 0) { $one = BigInteger::one(); return new BigRational($one, $one, false); } if ($exponent === 1) { return $this; } return new BigRational( $this->numerator->power($exponent), $this->denominator->power($exponent), false ); } /** * Returns the reciprocal of this BigRational. * * The reciprocal has the numerator and denominator swapped. * * @return BigRational * * @throws DivisionByZeroException If the numerator is zero. */ public function reciprocal() : BigRational { return new BigRational($this->denominator, $this->numerator, true); } /** * Returns the absolute value of this BigRational. * * @return BigRational */ public function abs() : BigRational { return new BigRational($this->numerator->abs(), $this->denominator, false); } /** * Returns the negated value of this BigRational. * * @return BigRational */ public function negated() : BigRational { return new BigRational($this->numerator->negated(), $this->denominator, false); } /** * Returns the simplified value of this BigRational. * * @return BigRational */ public function simplified() : BigRational { $gcd = $this->numerator->gcd($this->denominator); $numerator = $this->numerator->quotient($gcd); $denominator = $this->denominator->quotient($gcd); return new BigRational($numerator, $denominator, false); } /** * {@inheritdoc} */ public function compareTo($that) : int { return $this->minus($that)->getSign(); } /** * {@inheritdoc} */ public function getSign() : int { return $this->numerator->getSign(); } /** * {@inheritdoc} */ public function toBigInteger() : BigInteger { $simplified = $this->simplified(); if (! $simplified->denominator->isEqualTo(1)) { throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.'); } return $simplified->numerator; } /** * {@inheritdoc} */ public function toBigDecimal() : BigDecimal { return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); } /** * {@inheritdoc} */ public function toBigRational() : BigRational { return $this; } /** * {@inheritdoc} */ public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); } /** * {@inheritdoc} */ public function toInt() : int { return $this->toBigInteger()->toInt(); } /** * {@inheritdoc} */ public function toFloat() : float { return $this->numerator->toFloat() / $this->denominator->toFloat(); } /** * {@inheritdoc} */ public function __toString() : string { $numerator = (string) $this->numerator; $denominator = (string) $this->denominator; if ($denominator === '1') { return $numerator; } return $this->numerator . '/' . $this->denominator; } /** * This method is required for serializing the object and SHOULD NOT be accessed directly. * * @internal * * @return array{numerator: BigInteger, denominator: BigInteger} */ public function __serialize(): array { return ['numerator' => $this->numerator, 'denominator' => $this->denominator]; } /** * This method is only here to allow unserializing the object and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param array{numerator: BigInteger, denominator: BigInteger} $data * * @return void * * @throws \LogicException */ public function __unserialize(array $data): void { if (isset($this->numerator)) { throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); } $this->numerator = $data['numerator']; $this->denominator = $data['denominator']; } /** * This method is required by interface Serializable and SHOULD NOT be accessed directly. * * @internal * * @return string */ public function serialize() : string { return $this->numerator . '/' . $this->denominator; } /** * This method is only here to implement interface Serializable and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param string $value * * @return void * * @throws \LogicException */ public function unserialize($value) : void { if (isset($this->numerator)) { throw new \LogicException('unserialize() is an internal function, it must not be called directly.'); } [$numerator, $denominator] = \explode('/', $value); $this->numerator = BigInteger::of($numerator); $this->denominator = BigInteger::of($denominator); } } Exception/IntegerOverflowException.php 0000644 00000001144 15025063510 0014210 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; use Brick\Math\BigInteger; /** * Exception thrown when an integer overflow occurs. */ class IntegerOverflowException extends MathException { /** * @param BigInteger $value * * @return IntegerOverflowException * * @psalm-pure */ public static function toIntOverflow(BigInteger $value) : IntegerOverflowException { $message = '%s is out of range %d to %d and cannot be represented as an integer.'; return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX)); } } Exception/RoundingNecessaryException.php 0000644 00000000774 15025063510 0014541 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; /** * Exception thrown when a number cannot be represented at the requested scale without rounding. */ class RoundingNecessaryException extends MathException { /** * @return RoundingNecessaryException * * @psalm-pure */ public static function roundingNecessary() : RoundingNecessaryException { return new self('Rounding is necessary to represent the result of the operation at this scale.'); } } Exception/NegativeNumberException.php 0000644 00000000370 15025063510 0014002 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; /** * Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number. */ class NegativeNumberException extends MathException { } Exception/MathException.php 0000644 00000000414 15025063510 0011757 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; /** * Base class for all math exceptions. * * This class is abstract to ensure that only fine-grained exceptions are thrown throughout the code. */ class MathException extends \RuntimeException { } Exception/NumberFormatException.php 0000644 00000001437 15025063510 0013475 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; /** * Exception thrown when attempting to create a number from a string with an invalid format. */ class NumberFormatException extends MathException { /** * @param string $char The failing character. * * @return NumberFormatException * * @psalm-pure */ public static function charNotInAlphabet(string $char) : self { $ord = \ord($char); if ($ord < 32 || $ord > 126) { $char = \strtoupper(\dechex($ord)); if ($ord < 10) { $char = '0' . $char; } } else { $char = '"' . $char . '"'; } return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char)); } } Exception/DivisionByZeroException.php 0000644 00000001552 15025063510 0014011 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math\Exception; /** * Exception thrown when a division by zero occurs. */ class DivisionByZeroException extends MathException { /** * @return DivisionByZeroException * * @psalm-pure */ public static function divisionByZero() : DivisionByZeroException { return new self('Division by zero.'); } /** * @return DivisionByZeroException * * @psalm-pure */ public static function modulusMustNotBeZero() : DivisionByZeroException { return new self('The modulus must not be zero.'); } /** * @return DivisionByZeroException * * @psalm-pure */ public static function denominatorMustNotBeZero() : DivisionByZeroException { return new self('The denominator of a rational number cannot be zero.'); } } BigInteger.php 0000644 00000103602 15025063510 0007273 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math; use Brick\Math\Exception\DivisionByZeroException; use Brick\Math\Exception\IntegerOverflowException; use Brick\Math\Exception\MathException; use Brick\Math\Exception\NegativeNumberException; use Brick\Math\Exception\NumberFormatException; use Brick\Math\Internal\Calculator; /** * An arbitrary-size integer. * * All methods accepting a number as a parameter accept either a BigInteger instance, * an integer, or a string representing an arbitrary size integer. * * @psalm-immutable */ final class BigInteger extends BigNumber { /** * The value, as a string of digits with optional leading minus sign. * * No leading zeros must be present. * No leading minus sign must be present if the number is zero. * * @var string */ private $value; /** * Protected constructor. Use a factory method to obtain an instance. * * @param string $value A string of digits, with optional leading minus sign. */ protected function __construct(string $value) { $this->value = $value; } /** * Creates a BigInteger of the given value. * * @param BigNumber|int|float|string $value * * @return BigInteger * * @throws MathException If the value cannot be converted to a BigInteger. * * @psalm-pure */ public static function of($value) : BigNumber { return parent::of($value)->toBigInteger(); } /** * Creates a number from a string in a given base. * * The string can optionally be prefixed with the `+` or `-` sign. * * Bases greater than 36 are not supported by this method, as there is no clear consensus on which of the lowercase * or uppercase characters should come first. Instead, this method accepts any base up to 36, and does not * differentiate lowercase and uppercase characters, which are considered equal. * * For bases greater than 36, and/or custom alphabets, use the fromArbitraryBase() method. * * @param string $number The number to convert, in the given base. * @param int $base The base of the number, between 2 and 36. * * @return BigInteger * * @throws NumberFormatException If the number is empty, or contains invalid chars for the given base. * @throws \InvalidArgumentException If the base is out of range. * * @psalm-pure */ public static function fromBase(string $number, int $base) : BigInteger { if ($number === '') { throw new NumberFormatException('The number cannot be empty.'); } if ($base < 2 || $base > 36) { throw new \InvalidArgumentException(\sprintf('Base %d is not in range 2 to 36.', $base)); } if ($number[0] === '-') { $sign = '-'; $number = \substr($number, 1); } elseif ($number[0] === '+') { $sign = ''; $number = \substr($number, 1); } else { $sign = ''; } if ($number === '') { throw new NumberFormatException('The number cannot be empty.'); } $number = \ltrim($number, '0'); if ($number === '') { // The result will be the same in any base, avoid further calculation. return BigInteger::zero(); } if ($number === '1') { // The result will be the same in any base, avoid further calculation. return new BigInteger($sign . '1'); } $pattern = '/[^' . \substr(Calculator::ALPHABET, 0, $base) . ']/'; if (\preg_match($pattern, \strtolower($number), $matches) === 1) { throw new NumberFormatException(\sprintf('"%s" is not a valid character in base %d.', $matches[0], $base)); } if ($base === 10) { // The number is usable as is, avoid further calculation. return new BigInteger($sign . $number); } $result = Calculator::get()->fromBase($number, $base); return new BigInteger($sign . $result); } /** * Parses a string containing an integer in an arbitrary base, using a custom alphabet. * * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers. * * @param string $number The number to parse. * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. * * @return BigInteger * * @throws NumberFormatException If the given number is empty or contains invalid chars for the given alphabet. * @throws \InvalidArgumentException If the alphabet does not contain at least 2 chars. * * @psalm-pure */ public static function fromArbitraryBase(string $number, string $alphabet) : BigInteger { if ($number === '') { throw new NumberFormatException('The number cannot be empty.'); } $base = \strlen($alphabet); if ($base < 2) { throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.'); } $pattern = '/[^' . \preg_quote($alphabet, '/') . ']/'; if (\preg_match($pattern, $number, $matches) === 1) { throw NumberFormatException::charNotInAlphabet($matches[0]); } $number = Calculator::get()->fromArbitraryBase($number, $alphabet, $base); return new BigInteger($number); } /** * Translates a string of bytes containing the binary representation of a BigInteger into a BigInteger. * * The input string is assumed to be in big-endian byte-order: the most significant byte is in the zeroth element. * * If `$signed` is true, the input is assumed to be in two's-complement representation, and the leading bit is * interpreted as a sign bit. If `$signed` is false, the input is interpreted as an unsigned number, and the * resulting BigInteger will always be positive or zero. * * This method can be used to retrieve a number exported by `toBytes()`, as long as the `$signed` flags match. * * @param string $value The byte string. * @param bool $signed Whether to interpret as a signed number in two's-complement representation with a leading * sign bit. * * @return BigInteger * * @throws NumberFormatException If the string is empty. */ public static function fromBytes(string $value, bool $signed = true) : BigInteger { if ($value === '') { throw new NumberFormatException('The byte string must not be empty.'); } $twosComplement = false; if ($signed) { $x = \ord($value[0]); if (($twosComplement = ($x >= 0x80))) { $value = ~$value; } } $number = self::fromBase(\bin2hex($value), 16); if ($twosComplement) { return $number->plus(1)->negated(); } return $number; } /** * Generates a pseudo-random number in the range 0 to 2^numBits - 1. * * Using the default random bytes generator, this method is suitable for cryptographic use. * * @psalm-param callable(int): string $randomBytesGenerator * * @param int $numBits The number of bits. * @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, and returns a * string of random bytes of the given length. Defaults to the * `random_bytes()` function. * * @return BigInteger * * @throws \InvalidArgumentException If $numBits is negative. */ public static function randomBits(int $numBits, ?callable $randomBytesGenerator = null) : BigInteger { if ($numBits < 0) { throw new \InvalidArgumentException('The number of bits cannot be negative.'); } if ($numBits === 0) { return BigInteger::zero(); } if ($randomBytesGenerator === null) { $randomBytesGenerator = 'random_bytes'; } $byteLength = \intdiv($numBits - 1, 8) + 1; $extraBits = ($byteLength * 8 - $numBits); $bitmask = \chr(0xFF >> $extraBits); $randomBytes = $randomBytesGenerator($byteLength); $randomBytes[0] = $randomBytes[0] & $bitmask; return self::fromBytes($randomBytes, false); } /** * Generates a pseudo-random number between `$min` and `$max`. * * Using the default random bytes generator, this method is suitable for cryptographic use. * * @psalm-param (callable(int): string)|null $randomBytesGenerator * * @param BigNumber|int|float|string $min The lower bound. Must be convertible to a BigInteger. * @param BigNumber|int|float|string $max The upper bound. Must be convertible to a BigInteger. * @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, * and returns a string of random bytes of the given length. * Defaults to the `random_bytes()` function. * * @return BigInteger * * @throws MathException If one of the parameters cannot be converted to a BigInteger, * or `$min` is greater than `$max`. */ public static function randomRange($min, $max, ?callable $randomBytesGenerator = null) : BigInteger { $min = BigInteger::of($min); $max = BigInteger::of($max); if ($min->isGreaterThan($max)) { throw new MathException('$min cannot be greater than $max.'); } if ($min->isEqualTo($max)) { return $min; } $diff = $max->minus($min); $bitLength = $diff->getBitLength(); // try until the number is in range (50% to 100% chance of success) do { $randomNumber = self::randomBits($bitLength, $randomBytesGenerator); } while ($randomNumber->isGreaterThan($diff)); return $randomNumber->plus($min); } /** * Returns a BigInteger representing zero. * * @return BigInteger * * @psalm-pure */ public static function zero() : BigInteger { /** * @psalm-suppress ImpureStaticVariable * @var BigInteger|null $zero */ static $zero; if ($zero === null) { $zero = new BigInteger('0'); } return $zero; } /** * Returns a BigInteger representing one. * * @return BigInteger * * @psalm-pure */ public static function one() : BigInteger { /** * @psalm-suppress ImpureStaticVariable * @var BigInteger|null $one */ static $one; if ($one === null) { $one = new BigInteger('1'); } return $one; } /** * Returns a BigInteger representing ten. * * @return BigInteger * * @psalm-pure */ public static function ten() : BigInteger { /** * @psalm-suppress ImpureStaticVariable * @var BigInteger|null $ten */ static $ten; if ($ten === null) { $ten = new BigInteger('10'); } return $ten; } /** * Returns the sum of this number and the given one. * * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigInteger. * * @return BigInteger The result. * * @throws MathException If the number is not valid, or is not convertible to a BigInteger. */ public function plus($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '0') { return $this; } if ($this->value === '0') { return $that; } $value = Calculator::get()->add($this->value, $that->value); return new BigInteger($value); } /** * Returns the difference of this number and the given one. * * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigInteger. * * @return BigInteger The result. * * @throws MathException If the number is not valid, or is not convertible to a BigInteger. */ public function minus($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '0') { return $this; } $value = Calculator::get()->sub($this->value, $that->value); return new BigInteger($value); } /** * Returns the product of this number and the given one. * * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigInteger. * * @return BigInteger The result. * * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger. */ public function multipliedBy($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '1') { return $this; } if ($this->value === '1') { return $that; } $value = Calculator::get()->mul($this->value, $that->value); return new BigInteger($value); } /** * Returns the result of the division of this number by the given one. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * @param int $roundingMode An optional rounding mode. * * @return BigInteger The result. * * @throws MathException If the divisor is not a valid number, is not convertible to a BigInteger, is zero, * or RoundingMode::UNNECESSARY is used and the remainder is not zero. */ public function dividedBy($that, int $roundingMode = RoundingMode::UNNECESSARY) : BigInteger { $that = BigInteger::of($that); if ($that->value === '1') { return $this; } if ($that->value === '0') { throw DivisionByZeroException::divisionByZero(); } $result = Calculator::get()->divRound($this->value, $that->value, $roundingMode); return new BigInteger($result); } /** * Returns this number exponentiated to the given value. * * @param int $exponent The exponent. * * @return BigInteger The result. * * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. */ public function power(int $exponent) : BigInteger { if ($exponent === 0) { return BigInteger::one(); } if ($exponent === 1) { return $this; } if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { throw new \InvalidArgumentException(\sprintf( 'The exponent %d is not in the range 0 to %d.', $exponent, Calculator::MAX_POWER )); } return new BigInteger(Calculator::get()->pow($this->value, $exponent)); } /** * Returns the quotient of the division of this number by the given one. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return BigInteger * * @throws DivisionByZeroException If the divisor is zero. */ public function quotient($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '1') { return $this; } if ($that->value === '0') { throw DivisionByZeroException::divisionByZero(); } $quotient = Calculator::get()->divQ($this->value, $that->value); return new BigInteger($quotient); } /** * Returns the remainder of the division of this number by the given one. * * The remainder, when non-zero, has the same sign as the dividend. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return BigInteger * * @throws DivisionByZeroException If the divisor is zero. */ public function remainder($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '1') { return BigInteger::zero(); } if ($that->value === '0') { throw DivisionByZeroException::divisionByZero(); } $remainder = Calculator::get()->divR($this->value, $that->value); return new BigInteger($remainder); } /** * Returns the quotient and remainder of the division of this number by the given one. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return BigInteger[] An array containing the quotient and the remainder. * * @throws DivisionByZeroException If the divisor is zero. */ public function quotientAndRemainder($that) : array { $that = BigInteger::of($that); if ($that->value === '0') { throw DivisionByZeroException::divisionByZero(); } [$quotient, $remainder] = Calculator::get()->divQR($this->value, $that->value); return [ new BigInteger($quotient), new BigInteger($remainder) ]; } /** * Returns the modulo of this number and the given one. * * The modulo operation yields the same result as the remainder operation when both operands are of the same sign, * and may differ when signs are different. * * The result of the modulo operation, when non-zero, has the same sign as the divisor. * * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. * * @return BigInteger * * @throws DivisionByZeroException If the divisor is zero. */ public function mod($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '0') { throw DivisionByZeroException::modulusMustNotBeZero(); } $value = Calculator::get()->mod($this->value, $that->value); return new BigInteger($value); } /** * Returns the modular multiplicative inverse of this BigInteger modulo $m. * * @param BigInteger $m * * @return BigInteger * * @throws DivisionByZeroException If $m is zero. * @throws NegativeNumberException If $m is negative. * @throws MathException If this BigInteger has no multiplicative inverse mod m (that is, this BigInteger * is not relatively prime to m). */ public function modInverse(BigInteger $m) : BigInteger { if ($m->value === '0') { throw DivisionByZeroException::modulusMustNotBeZero(); } if ($m->isNegative()) { throw new NegativeNumberException('Modulus must not be negative.'); } if ($m->value === '1') { return BigInteger::zero(); } $value = Calculator::get()->modInverse($this->value, $m->value); if ($value === null) { throw new MathException('Unable to compute the modInverse for the given modulus.'); } return new BigInteger($value); } /** * Returns this number raised into power with modulo. * * This operation only works on positive numbers. * * @param BigNumber|int|float|string $exp The exponent. Must be positive or zero. * @param BigNumber|int|float|string $mod The modulus. Must be strictly positive. * * @return BigInteger * * @throws NegativeNumberException If any of the operands is negative. * @throws DivisionByZeroException If the modulus is zero. */ public function modPow($exp, $mod) : BigInteger { $exp = BigInteger::of($exp); $mod = BigInteger::of($mod); if ($this->isNegative() || $exp->isNegative() || $mod->isNegative()) { throw new NegativeNumberException('The operands cannot be negative.'); } if ($mod->isZero()) { throw DivisionByZeroException::modulusMustNotBeZero(); } $result = Calculator::get()->modPow($this->value, $exp->value, $mod->value); return new BigInteger($result); } /** * Returns the greatest common divisor of this number and the given one. * * The GCD is always positive, unless both operands are zero, in which case it is zero. * * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * * @return BigInteger */ public function gcd($that) : BigInteger { $that = BigInteger::of($that); if ($that->value === '0' && $this->value[0] !== '-') { return $this; } if ($this->value === '0' && $that->value[0] !== '-') { return $that; } $value = Calculator::get()->gcd($this->value, $that->value); return new BigInteger($value); } /** * Returns the integer square root number of this number, rounded down. * * The result is the largest x such that x² ≤ n. * * @return BigInteger * * @throws NegativeNumberException If this number is negative. */ public function sqrt() : BigInteger { if ($this->value[0] === '-') { throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); } $value = Calculator::get()->sqrt($this->value); return new BigInteger($value); } /** * Returns the absolute value of this number. * * @return BigInteger */ public function abs() : BigInteger { return $this->isNegative() ? $this->negated() : $this; } /** * Returns the inverse of this number. * * @return BigInteger */ public function negated() : BigInteger { return new BigInteger(Calculator::get()->neg($this->value)); } /** * Returns the integer bitwise-and combined with another integer. * * This method returns a negative BigInteger if and only if both operands are negative. * * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * * @return BigInteger */ public function and($that) : BigInteger { $that = BigInteger::of($that); return new BigInteger(Calculator::get()->and($this->value, $that->value)); } /** * Returns the integer bitwise-or combined with another integer. * * This method returns a negative BigInteger if and only if either of the operands is negative. * * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * * @return BigInteger */ public function or($that) : BigInteger { $that = BigInteger::of($that); return new BigInteger(Calculator::get()->or($this->value, $that->value)); } /** * Returns the integer bitwise-xor combined with another integer. * * This method returns a negative BigInteger if and only if exactly one of the operands is negative. * * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. * * @return BigInteger */ public function xor($that) : BigInteger { $that = BigInteger::of($that); return new BigInteger(Calculator::get()->xor($this->value, $that->value)); } /** * Returns the bitwise-not of this BigInteger. * * @return BigInteger */ public function not() : BigInteger { return $this->negated()->minus(1); } /** * Returns the integer left shifted by a given number of bits. * * @param int $distance The distance to shift. * * @return BigInteger */ public function shiftedLeft(int $distance) : BigInteger { if ($distance === 0) { return $this; } if ($distance < 0) { return $this->shiftedRight(- $distance); } return $this->multipliedBy(BigInteger::of(2)->power($distance)); } /** * Returns the integer right shifted by a given number of bits. * * @param int $distance The distance to shift. * * @return BigInteger */ public function shiftedRight(int $distance) : BigInteger { if ($distance === 0) { return $this; } if ($distance < 0) { return $this->shiftedLeft(- $distance); } $operand = BigInteger::of(2)->power($distance); if ($this->isPositiveOrZero()) { return $this->quotient($operand); } return $this->dividedBy($operand, RoundingMode::UP); } /** * Returns the number of bits in the minimal two's-complement representation of this BigInteger, excluding a sign bit. * * For positive BigIntegers, this is equivalent to the number of bits in the ordinary binary representation. * Computes (ceil(log2(this < 0 ? -this : this+1))). * * @return int */ public function getBitLength() : int { if ($this->value === '0') { return 0; } if ($this->isNegative()) { return $this->abs()->minus(1)->getBitLength(); } return \strlen($this->toBase(2)); } /** * Returns the index of the rightmost (lowest-order) one bit in this BigInteger. * * Returns -1 if this BigInteger contains no one bits. * * @return int */ public function getLowestSetBit() : int { $n = $this; $bitLength = $this->getBitLength(); for ($i = 0; $i <= $bitLength; $i++) { if ($n->isOdd()) { return $i; } $n = $n->shiftedRight(1); } return -1; } /** * Returns whether this number is even. * * @return bool */ public function isEven() : bool { return \in_array($this->value[-1], ['0', '2', '4', '6', '8'], true); } /** * Returns whether this number is odd. * * @return bool */ public function isOdd() : bool { return \in_array($this->value[-1], ['1', '3', '5', '7', '9'], true); } /** * Returns true if and only if the designated bit is set. * * Computes ((this & (1<<n)) != 0). * * @param int $n The bit to test, 0-based. * * @return bool * * @throws \InvalidArgumentException If the bit to test is negative. */ public function testBit(int $n) : bool { if ($n < 0) { throw new \InvalidArgumentException('The bit to test cannot be negative.'); } return $this->shiftedRight($n)->isOdd(); } /** * {@inheritdoc} */ public function compareTo($that) : int { $that = BigNumber::of($that); if ($that instanceof BigInteger) { return Calculator::get()->cmp($this->value, $that->value); } return - $that->compareTo($this); } /** * {@inheritdoc} */ public function getSign() : int { return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); } /** * {@inheritdoc} */ public function toBigInteger() : BigInteger { return $this; } /** * {@inheritdoc} */ public function toBigDecimal() : BigDecimal { return BigDecimal::create($this->value); } /** * {@inheritdoc} */ public function toBigRational() : BigRational { return BigRational::create($this, BigInteger::one(), false); } /** * {@inheritdoc} */ public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { return $this->toBigDecimal()->toScale($scale, $roundingMode); } /** * {@inheritdoc} */ public function toInt() : int { $intValue = (int) $this->value; if ($this->value !== (string) $intValue) { throw IntegerOverflowException::toIntOverflow($this); } return $intValue; } /** * {@inheritdoc} */ public function toFloat() : float { return (float) $this->value; } /** * Returns a string representation of this number in the given base. * * The output will always be lowercase for bases greater than 10. * * @param int $base * * @return string * * @throws \InvalidArgumentException If the base is out of range. */ public function toBase(int $base) : string { if ($base === 10) { return $this->value; } if ($base < 2 || $base > 36) { throw new \InvalidArgumentException(\sprintf('Base %d is out of range [2, 36]', $base)); } return Calculator::get()->toBase($this->value, $base); } /** * Returns a string representation of this number in an arbitrary base with a custom alphabet. * * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers; * a NegativeNumberException will be thrown when attempting to call this method on a negative number. * * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. * * @return string * * @throws NegativeNumberException If this number is negative. * @throws \InvalidArgumentException If the given alphabet does not contain at least 2 chars. */ public function toArbitraryBase(string $alphabet) : string { $base = \strlen($alphabet); if ($base < 2) { throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.'); } if ($this->value[0] === '-') { throw new NegativeNumberException(__FUNCTION__ . '() does not support negative numbers.'); } return Calculator::get()->toArbitraryBase($this->value, $alphabet, $base); } /** * Returns a string of bytes containing the binary representation of this BigInteger. * * The string is in big-endian byte-order: the most significant byte is in the zeroth element. * * If `$signed` is true, the output will be in two's-complement representation, and a sign bit will be prepended to * the output. If `$signed` is false, no sign bit will be prepended, and this method will throw an exception if the * number is negative. * * The string will contain the minimum number of bytes required to represent this BigInteger, including a sign bit * if `$signed` is true. * * This representation is compatible with the `fromBytes()` factory method, as long as the `$signed` flags match. * * @param bool $signed Whether to output a signed number in two's-complement representation with a leading sign bit. * * @return string * * @throws NegativeNumberException If $signed is false, and the number is negative. */ public function toBytes(bool $signed = true) : string { if (! $signed && $this->isNegative()) { throw new NegativeNumberException('Cannot convert a negative number to a byte string when $signed is false.'); } $hex = $this->abs()->toBase(16); if (\strlen($hex) % 2 !== 0) { $hex = '0' . $hex; } $baseHexLength = \strlen($hex); if ($signed) { if ($this->isNegative()) { $bin = \hex2bin($hex); assert($bin !== false); $hex = \bin2hex(~$bin); $hex = self::fromBase($hex, 16)->plus(1)->toBase(16); $hexLength = \strlen($hex); if ($hexLength < $baseHexLength) { $hex = \str_repeat('0', $baseHexLength - $hexLength) . $hex; } if ($hex[0] < '8') { $hex = 'FF' . $hex; } } else { if ($hex[0] >= '8') { $hex = '00' . $hex; } } } return \hex2bin($hex); } /** * {@inheritdoc} */ public function __toString() : string { return $this->value; } /** * This method is required for serializing the object and SHOULD NOT be accessed directly. * * @internal * * @return array{value: string} */ public function __serialize(): array { return ['value' => $this->value]; } /** * This method is only here to allow unserializing the object and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param array{value: string} $data * * @return void * * @throws \LogicException */ public function __unserialize(array $data): void { if (isset($this->value)) { throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); } $this->value = $data['value']; } /** * This method is required by interface Serializable and SHOULD NOT be accessed directly. * * @internal * * @return string */ public function serialize() : string { return $this->value; } /** * This method is only here to implement interface Serializable and cannot be accessed directly. * * @internal * @psalm-suppress RedundantPropertyInitializationCheck * * @param string $value * * @return void * * @throws \LogicException */ public function unserialize($value) : void { if (isset($this->value)) { throw new \LogicException('unserialize() is an internal function, it must not be called directly.'); } $this->value = $value; } } RoundingMode.php 0000644 00000007423 15025063510 0007652 0 ustar 00 <?php declare(strict_types=1); namespace Brick\Math; /** * Specifies a rounding behavior for numerical operations capable of discarding precision. * * Each rounding mode indicates how the least significant returned digit of a rounded result * is to be calculated. If fewer digits are returned than the digits needed to represent the * exact numerical result, the discarded digits will be referred to as the discarded fraction * regardless the digits' contribution to the value of the number. In other words, considered * as a numerical value, the discarded fraction could have an absolute value greater than one. */ final class RoundingMode { /** * Private constructor. This class is not instantiable. * * @codeCoverageIgnore */ private function __construct() { } /** * Asserts that the requested operation has an exact result, hence no rounding is necessary. * * If this rounding mode is specified on an operation that yields a result that * cannot be represented at the requested scale, a RoundingNecessaryException is thrown. */ public const UNNECESSARY = 0; /** * Rounds away from zero. * * Always increments the digit prior to a nonzero discarded fraction. * Note that this rounding mode never decreases the magnitude of the calculated value. */ public const UP = 1; /** * Rounds towards zero. * * Never increments the digit prior to a discarded fraction (i.e., truncates). * Note that this rounding mode never increases the magnitude of the calculated value. */ public const DOWN = 2; /** * Rounds towards positive infinity. * * If the result is positive, behaves as for UP; if negative, behaves as for DOWN. * Note that this rounding mode never decreases the calculated value. */ public const CEILING = 3; /** * Rounds towards negative infinity. * * If the result is positive, behave as for DOWN; if negative, behave as for UP. * Note that this rounding mode never increases the calculated value. */ public const FLOOR = 4; /** * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. * * Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN. * Note that this is the rounding mode commonly taught at school. */ public const HALF_UP = 5; /** * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. * * Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN. */ public const HALF_DOWN = 6; /** * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity. * * If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN. */ public const HALF_CEILING = 7; /** * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity. * * If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP. */ public const HALF_FLOOR = 8; /** * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor. * * Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd; * behaves as for HALF_DOWN if it's even. * * Note that this is the rounding mode that statistically minimizes * cumulative error when applied repeatedly over a sequence of calculations. * It is sometimes known as "Banker's rounding", and is chiefly used in the USA. */ public const HALF_EVEN = 9; } Parser/Entry.php 0000644 00000001766 15025063706 0007630 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; use PhpOption\Option; final class Entry { /** * The entry name. * * @var string */ private $name; /** * The entry value. * * @var \Dotenv\Parser\Value|null */ private $value; /** * Create a new entry instance. * * @param string $name * @param \Dotenv\Parser\Value|null $value * * @return void */ public function __construct(string $name, Value $value = null) { $this->name = $name; $this->value = $value; } /** * Get the entry name. * * @return string */ public function getName() { return $this->name; } /** * Get the entry value. * * @return \PhpOption\Option<\Dotenv\Parser\Value> */ public function getValue() { /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ return Option::fromValue($this->value); } } Parser/ParserInterface.php 0000644 00000000516 15025063706 0011574 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; interface ParserInterface { /** * Parse content into an entry array. * * @param string $content * * @throws \Dotenv\Exception\InvalidFileException * * @return \Dotenv\Parser\Entry[] */ public function parse(string $content); } Parser/Lexer.php 0000644 00000002370 15025063706 0007576 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; final class Lexer { /** * The regex for each type of token. */ private const PATTERNS = [ '[\r\n]{1,1000}', '[^\S\r\n]{1,1000}', '\\\\', '\'', '"', '\\#', '\\$', '([^(\s\\\\\'"\\#\\$)]|\\(|\\)){1,1000}', ]; /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Convert content into a token stream. * * Multibyte string processing is not needed here, and nether is error * handling, for performance reasons. * * @param string $content * * @return \Generator<string> */ public static function lex(string $content) { static $regex; if ($regex === null) { $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; } $offset = 0; while (isset($content[$offset])) { if (!\preg_match($regex, $content, $matches, 0, $offset)) { throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); } $offset += \strlen($matches[0]); yield $matches[0]; } } } Parser/Lines.php 0000644 00000005776 15025063706 0007606 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; use Dotenv\Util\Regex; use Dotenv\Util\Str; final class Lines { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Process the array of lines of environment variables. * * This will produce an array of raw entries, one per variable. * * @param string[] $lines * * @return string[] */ public static function process(array $lines) { $output = []; $multiline = false; $multilineBuffer = []; foreach ($lines as $line) { [$multiline, $line, $multilineBuffer] = self::multilineProcess($multiline, $line, $multilineBuffer); if (!$multiline && !self::isCommentOrWhitespace($line)) { $output[] = $line; } } return $output; } /** * Used to make all multiline variable process. * * @param bool $multiline * @param string $line * @param string[] $buffer * * @return array{bool,string,string[]} */ private static function multilineProcess(bool $multiline, string $line, array $buffer) { // check if $line can be multiline variable if ($started = self::looksLikeMultilineStart($line)) { $multiline = true; } if ($multiline) { \array_push($buffer, $line); if (self::looksLikeMultilineStop($line, $started)) { $multiline = false; $line = \implode("\n", $buffer); $buffer = []; } } return [$multiline, $line, $buffer]; } /** * Determine if the given line can be the start of a multiline variable. * * @param string $line * * @return bool */ private static function looksLikeMultilineStart(string $line) { return Str::pos($line, '="')->map(static function () use ($line) { return self::looksLikeMultilineStop($line, true) === false; })->getOrElse(false); } /** * Determine if the given line can be the start of a multiline variable. * * @param string $line * @param bool $started * * @return bool */ private static function looksLikeMultilineStop(string $line, bool $started) { if ($line === '"') { return true; } return Regex::occurences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { return $started ? $count > 1 : $count >= 1; })->success()->getOrElse(false); } /** * Determine if the line in the file is a comment or whitespace. * * @param string $line * * @return bool */ private static function isCommentOrWhitespace(string $line) { $line = \trim($line); return $line === '' || (isset($line[0]) && $line[0] === '#'); } } Parser/EntryParser.php 0000644 00000027134 15025063706 0011002 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; use Dotenv\Util\Regex; use Dotenv\Util\Str; use GrahamCampbell\ResultType\Error; use GrahamCampbell\ResultType\Result; use GrahamCampbell\ResultType\Success; final class EntryParser { private const INITIAL_STATE = 0; private const UNQUOTED_STATE = 1; private const SINGLE_QUOTED_STATE = 2; private const DOUBLE_QUOTED_STATE = 3; private const ESCAPE_SEQUENCE_STATE = 4; private const WHITESPACE_STATE = 5; private const COMMENT_STATE = 6; private const REJECT_STATES = [self::SINGLE_QUOTED_STATE, self::DOUBLE_QUOTED_STATE, self::ESCAPE_SEQUENCE_STATE]; /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Parse a raw entry into a proper entry. * * That is, turn a raw environment variable entry into a name and possibly * a value. We wrap the answer in a result type. * * @param string $entry * * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string> */ public static function parse(string $entry) { return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { [$name, $value] = $parts; return self::parseName($name)->flatMap(static function (string $name) use ($value) { /** @var Result<Value|null,string> */ $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); return $parsedValue->map(static function (?Value $value) use ($name) { return new Entry($name, $value); }); }); }); } /** * Split the compound string into parts. * * @param string $line * * @return \GrahamCampbell\ResultType\Result<array{string,string|null},string> */ private static function splitStringIntoParts(string $line) { /** @var array{string,string|null} */ $result = Str::pos($line, '=')->map(static function () use ($line) { return \array_map('trim', \explode('=', $line, 2)); })->getOrElse([$line, null]); if ($result[0] === '') { return Error::create(self::getErrorMessage('an unexpected equals', $line)); } /** @var \GrahamCampbell\ResultType\Result<array{string,string|null},string> */ return Success::create($result); } /** * Parse the given variable name. * * That is, strip the optional quotes and leading "export" from the * variable name. We wrap the answer in a result type. * * @param string $name * * @return \GrahamCampbell\ResultType\Result<string,string> */ private static function parseName(string $name) { if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { $name = \ltrim(Str::substr($name, 6)); } if (self::isQuotedName($name)) { $name = Str::substr($name, 1, -1); } if (!self::isValidName($name)) { return Error::create(self::getErrorMessage('an invalid name', $name)); } return Success::create($name); } /** * Is the given variable name quoted? * * @param string $name * * @return bool */ private static function isQuotedName(string $name) { if (Str::len($name) < 3) { return false; } $first = Str::substr($name, 0, 1); $last = Str::substr($name, -1, 1); return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); } /** * Is the given variable name valid? * * @param string $name * * @return bool */ private static function isValidName(string $name) { return Regex::matches('~\A[a-zA-Z0-9_.]+\z~', $name)->success()->getOrElse(false); } /** * Parse the given variable value. * * This has the effect of stripping quotes and comments, dealing with * special characters, and locating nested variables, but not resolving * them. Formally, we run a finite state automaton with an output tape: a * transducer. We wrap the answer in a result type. * * @param string $value * * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ private static function parseValue(string $value) { if (\trim($value) === '') { return Success::create(Value::blank()); } return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { return $data->flatMap(static function (array $data) use ($token) { return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { return [$data[0]->append($val[0], $val[1]), $val[2]]; }); }); }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { if (in_array($result[1], self::REJECT_STATES, true)) { return Error::create('a missing closing quote'); } return Success::create($result[0]); })->mapError(static function (string $err) use ($value) { return self::getErrorMessage($err, $value); }); } /** * Process the given token. * * @param int $state * @param string $token * * @return \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ private static function processToken(int $state, string $token) { switch ($state) { case self::INITIAL_STATE: if ($token === '\'') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::SINGLE_QUOTED_STATE]); } elseif ($token === '"') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); } elseif ($token === '#') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::COMMENT_STATE]); } elseif ($token === '$') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, true, self::UNQUOTED_STATE]); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::UNQUOTED_STATE]); } case self::UNQUOTED_STATE: if ($token === '#') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::COMMENT_STATE]); } elseif (\ctype_space($token)) { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::WHITESPACE_STATE]); } elseif ($token === '$') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, true, self::UNQUOTED_STATE]); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::UNQUOTED_STATE]); } case self::SINGLE_QUOTED_STATE: if ($token === '\'') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::WHITESPACE_STATE]); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); } case self::DOUBLE_QUOTED_STATE: if ($token === '"') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::WHITESPACE_STATE]); } elseif ($token === '\\') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); } elseif ($token === '$') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); } case self::ESCAPE_SEQUENCE_STATE: if ($token === '"' || $token === '\\') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); } elseif ($token === '$') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); } else { $first = Str::substr($token, 0, 1); if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Error::create('an unexpected escape sequence'); } } case self::WHITESPACE_STATE: if ($token === '#') { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::COMMENT_STATE]); } elseif (!\ctype_space($token)) { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Error::create('unexpected whitespace'); } else { /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::WHITESPACE_STATE]); } case self::COMMENT_STATE: /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */ return Success::create(['', false, self::COMMENT_STATE]); default: throw new \Error('Parser entered invalid state.'); } } /** * Generate a friendly error message. * * @param string $cause * @param string $subject * * @return string */ private static function getErrorMessage(string $cause, string $subject) { return \sprintf( 'Encountered %s at [%s].', $cause, \strtok($subject, "\n") ); } } Parser/Value.php 0000644 00000003064 15025063706 0007574 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; use Dotenv\Util\Str; final class Value { /** * The string representation of the parsed value. * * @var string */ private $chars; /** * The locations of the variables in the value. * * @var int[] */ private $vars; /** * Internal constructor for a value. * * @param string $chars * @param int[] $vars * * @return void */ private function __construct(string $chars, array $vars) { $this->chars = $chars; $this->vars = $vars; } /** * Create an empty value instance. * * @return \Dotenv\Parser\Value */ public static function blank() { return new self('', []); } /** * Create a new value instance, appending the characters. * * @param string $chars * @param bool $var * * @return \Dotenv\Parser\Value */ public function append(string $chars, bool $var) { return new self( $this->chars.$chars, $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars ); } /** * Get the string representation of the parsed value. * * @return string */ public function getChars() { return $this->chars; } /** * Get the locations of the variables in the value. * * @return int[] */ public function getVars() { $vars = $this->vars; \rsort($vars); return $vars; } } Parser/Parser.php 0000644 00000003234 15025063706 0007753 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Parser; use Dotenv\Exception\InvalidFileException; use Dotenv\Util\Regex; use GrahamCampbell\ResultType\Result; use GrahamCampbell\ResultType\Success; final class Parser implements ParserInterface { /** * Parse content into an entry array. * * @param string $content * * @throws \Dotenv\Exception\InvalidFileException * * @return \Dotenv\Parser\Entry[] */ public function parse(string $content) { return Regex::split("/(\r\n|\n|\r)/", $content)->mapError(static function () { return 'Could not split into separate lines.'; })->flatMap(static function (array $lines) { return self::process(Lines::process($lines)); })->mapError(static function (string $error) { throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); })->success()->get(); } /** * Convert the raw entries into proper entries. * * @param string[] $entries * * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> */ private static function process(array $entries) { /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> */ return \array_reduce($entries, static function (Result $result, string $raw) { return $result->flatMap(static function (array $entries) use ($raw) { return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { return \array_merge($entries, [$entry]); }); }); }, Success::create([])); } } Repository/AdapterRepository.php 0000644 00000003426 15025063706 0013125 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository; use Dotenv\Repository\Adapter\ReaderInterface; use Dotenv\Repository\Adapter\WriterInterface; final class AdapterRepository implements RepositoryInterface { /** * The reader to use. * * @var \Dotenv\Repository\Adapter\ReaderInterface */ private $reader; /** * The writer to use. * * @var \Dotenv\Repository\Adapter\WriterInterface */ private $writer; /** * Create a new adapter repository instance. * * @param \Dotenv\Repository\Adapter\ReaderInterface $reader * @param \Dotenv\Repository\Adapter\WriterInterface $writer * * @return void */ public function __construct(ReaderInterface $reader, WriterInterface $writer) { $this->reader = $reader; $this->writer = $writer; } /** * Determine if the given environment variable is defined. * * @param string $name * * @return bool */ public function has(string $name) { return $this->reader->read($name)->isDefined(); } /** * Get an environment variable. * * @param string $name * * @return string|null */ public function get(string $name) { return $this->reader->read($name)->getOrElse(null); } /** * Set an environment variable. * * @param string $name * @param string $value * * @return bool */ public function set(string $name, string $value) { return $this->writer->write($name, $value); } /** * Clear an environment variable. * * @param string $name * * @return bool */ public function clear(string $name) { return $this->writer->delete($name); } } Repository/RepositoryInterface.php 0000644 00000001425 15025063706 0013442 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository; interface RepositoryInterface { /** * Determine if the given environment variable is defined. * * @param string $name * * @return bool */ public function has(string $name); /** * Get an environment variable. * * @param string $name * * @return string|null */ public function get(string $name); /** * Set an environment variable. * * @param string $name * @param string $value * * @return bool */ public function set(string $name, string $value); /** * Clear an environment variable. * * @param string $name * * @return bool */ public function clear(string $name); } Repository/Adapter/AdapterInterface.php 0000644 00000000532 15025063706 0014221 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; interface AdapterInterface extends ReaderInterface, WriterInterface { /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create(); } Repository/Adapter/ReaderInterface.php 0000644 00000000442 15025063706 0014043 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; interface ReaderInterface { /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name); } Repository/Adapter/PutenvAdapter.php 0000644 00000003475 15025063706 0013613 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\None; use PhpOption\Option; use PhpOption\Some; final class PutenvAdapter implements AdapterInterface { /** * Create a new putenv adapter instance. * * @return void */ private function __construct() { // } /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create() { if (self::isSupported()) { /** @var \PhpOption\Option<AdapterInterface> */ return Some::create(new self()); } return None::create(); } /** * Determines if the adapter is supported. * * @return bool */ private static function isSupported() { return \function_exists('getenv') && \function_exists('putenv'); } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { /** @var \PhpOption\Option<string> */ return Option::fromValue(\getenv($name), false)->filter(static function ($value) { return \is_string($value); }); } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { \putenv("$name=$value"); return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { \putenv($name); return true; } } Repository/Adapter/ServerConstAdapter.php 0000644 00000003455 15025063706 0014605 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\Option; use PhpOption\Some; final class ServerConstAdapter implements AdapterInterface { /** * Create a new server const adapter instance. * * @return void */ private function __construct() { // } /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create() { /** @var \PhpOption\Option<AdapterInterface> */ return Some::create(new self()); } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { /** @var \PhpOption\Option<string> */ return Option::fromArraysValue($_SERVER, $name) ->map(static function ($value) { if ($value === false) { return 'false'; } if ($value === true) { return 'true'; } return $value; })->filter(static function ($value) { return \is_string($value); }); } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { $_SERVER[$name] = $value; return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { unset($_SERVER[$name]); return true; } } Repository/Adapter/GuardedWriter.php 0000644 00000003453 15025063706 0013575 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; final class GuardedWriter implements WriterInterface { /** * The inner writer to use. * * @var \Dotenv\Repository\Adapter\WriterInterface */ private $writer; /** * The variable name allow list. * * @var string[] */ private $allowList; /** * Create a new guarded writer instance. * * @param \Dotenv\Repository\Adapter\WriterInterface $writer * @param string[] $allowList * * @return void */ public function __construct(WriterInterface $writer, array $allowList) { $this->writer = $writer; $this->allowList = $allowList; } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { // Don't set non-allowed variables if (!$this->isAllowed($name)) { return false; } // Set the value on the inner writer return $this->writer->write($name, $value); } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { // Don't clear non-allowed variables if (!$this->isAllowed($name)) { return false; } // Set the value on the inner writer return $this->writer->delete($name); } /** * Determine if the given variable is allowed. * * @param string $name * * @return bool */ private function isAllowed(string $name) { return \in_array($name, $this->allowList, true); } } Repository/Adapter/ImmutableWriter.php 0000644 00000004706 15025063706 0014143 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; final class ImmutableWriter implements WriterInterface { /** * The inner writer to use. * * @var \Dotenv\Repository\Adapter\WriterInterface */ private $writer; /** * The inner reader to use. * * @var \Dotenv\Repository\Adapter\ReaderInterface */ private $reader; /** * The record of loaded variables. * * @var array<string,string> */ private $loaded; /** * Create a new immutable writer instance. * * @param \Dotenv\Repository\Adapter\WriterInterface $writer * @param \Dotenv\Repository\Adapter\ReaderInterface $reader * * @return void */ public function __construct(WriterInterface $writer, ReaderInterface $reader) { $this->writer = $writer; $this->reader = $reader; $this->loaded = []; } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { // Don't overwrite existing environment variables // Ruby's dotenv does this with `ENV[key] ||= value` if ($this->isExternallyDefined($name)) { return false; } // Set the value on the inner writer if (!$this->writer->write($name, $value)) { return false; } // Record that we have loaded the variable $this->loaded[$name] = ''; return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { // Don't clear existing environment variables if ($this->isExternallyDefined($name)) { return false; } // Clear the value on the inner writer if (!$this->writer->delete($name)) { return false; } // Leave the variable as fair game unset($this->loaded[$name]); return true; } /** * Determine if the given variable is externally defined. * * That is, is it an "existing" variable. * * @param string $name * * @return bool */ private function isExternallyDefined(string $name) { return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); } } Repository/Adapter/EnvConstAdapter.php 0000644 00000003436 15025063706 0014066 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\Option; use PhpOption\Some; final class EnvConstAdapter implements AdapterInterface { /** * Create a new env const adapter instance. * * @return void */ private function __construct() { // } /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create() { /** @var \PhpOption\Option<AdapterInterface> */ return Some::create(new self()); } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { /** @var \PhpOption\Option<string> */ return Option::fromArraysValue($_ENV, $name) ->map(static function ($value) { if ($value === false) { return 'false'; } if ($value === true) { return 'true'; } return $value; })->filter(static function ($value) { return \is_string($value); }); } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { $_ENV[$name] = $value; return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { unset($_ENV[$name]); return true; } } Repository/Adapter/ApacheAdapter.php 0000644 00000003616 15025063706 0013510 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\None; use PhpOption\Option; use PhpOption\Some; final class ApacheAdapter implements AdapterInterface { /** * Create a new apache adapter instance. * * @return void */ private function __construct() { // } /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create() { if (self::isSupported()) { /** @var \PhpOption\Option<AdapterInterface> */ return Some::create(new self()); } return None::create(); } /** * Determines if the adapter is supported. * * This happens if PHP is running as an Apache module. * * @return bool */ private static function isSupported() { return \function_exists('apache_getenv') && \function_exists('apache_setenv'); } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { /** @var \PhpOption\Option<string> */ return Option::fromValue(apache_getenv($name))->filter(static function ($value) { return \is_string($value) && $value !== ''; }); } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { return apache_setenv($name, $value); } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { return apache_setenv($name, ''); } } Repository/Adapter/ArrayAdapter.php 0000644 00000003042 15025063706 0013376 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\Option; use PhpOption\Some; final class ArrayAdapter implements AdapterInterface { /** * The variables and their values. * * @var array<string,string> */ private $variables; /** * Create a new array adapter instance. * * @return void */ private function __construct() { $this->variables = []; } /** * Create a new instance of the adapter, if it is available. * * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> */ public static function create() { /** @var \PhpOption\Option<AdapterInterface> */ return Some::create(new self()); } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { return Option::fromArraysValue($this->variables, $name); } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { $this->variables[$name] = $value; return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { unset($this->variables[$name]); return true; } } Repository/Adapter/MultiWriter.php 0000644 00000002366 15025063706 0013316 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; final class MultiWriter implements WriterInterface { /** * The set of writers to use. * * @var \Dotenv\Repository\Adapter\WriterInterface[] */ private $writers; /** * Create a new multi-writer instance. * * @param \Dotenv\Repository\Adapter\WriterInterface[] $writers * * @return void */ public function __construct(array $writers) { $this->writers = $writers; } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { foreach ($this->writers as $writers) { if (!$writers->write($name, $value)) { return false; } } return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { foreach ($this->writers as $writers) { if (!$writers->delete($name)) { return false; } } return true; } } Repository/Adapter/MultiReader.php 0000644 00000001677 15025063706 0013250 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; use PhpOption\None; final class MultiReader implements ReaderInterface { /** * The set of readers to use. * * @var \Dotenv\Repository\Adapter\ReaderInterface[] */ private $readers; /** * Create a new multi-reader instance. * * @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers * * @return void */ public function __construct(array $readers) { $this->readers = $readers; } /** * Read an environment variable, if it exists. * * @param string $name * * @return \PhpOption\Option<string> */ public function read(string $name) { foreach ($this->readers as $reader) { $result = $reader->read($name); if ($result->isDefined()) { return $result; } } return None::create(); } } Repository/Adapter/ReplacingWriter.php 0000644 00000004173 15025063706 0014126 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; final class ReplacingWriter implements WriterInterface { /** * The inner writer to use. * * @var \Dotenv\Repository\Adapter\WriterInterface */ private $writer; /** * The inner reader to use. * * @var \Dotenv\Repository\Adapter\ReaderInterface */ private $reader; /** * The record of seen variables. * * @var array<string,string> */ private $seen; /** * Create a new replacement writer instance. * * @param \Dotenv\Repository\Adapter\WriterInterface $writer * @param \Dotenv\Repository\Adapter\ReaderInterface $reader * * @return void */ public function __construct(WriterInterface $writer, ReaderInterface $reader) { $this->writer = $writer; $this->reader = $reader; $this->seen = []; } /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value) { if ($this->exists($name)) { return $this->writer->write($name, $value); } // succeed if nothing to do return true; } /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name) { if ($this->exists($name)) { return $this->writer->delete($name); } // succeed if nothing to do return true; } /** * Does the given environment variable exist. * * Returns true if it currently exists, or existed at any point in the past * that we are aware of. * * @param string $name * * @return bool */ private function exists(string $name) { if (isset($this->seen[$name])) { return true; } if ($this->reader->read($name)->isDefined()) { $this->seen[$name] = ''; return true; } return false; } } Repository/Adapter/WriterInterface.php 0000644 00000000750 15025063706 0014117 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository\Adapter; interface WriterInterface { /** * Write to an environment variable, if possible. * * @param string $name * @param string $value * * @return bool */ public function write(string $name, string $value); /** * Delete an environment variable, if possible. * * @param string $name * * @return bool */ public function delete(string $name); } Repository/RepositoryBuilder.php 0000644 00000020032 15025063706 0013123 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Repository; use Dotenv\Repository\Adapter\AdapterInterface; use Dotenv\Repository\Adapter\EnvConstAdapter; use Dotenv\Repository\Adapter\GuardedWriter; use Dotenv\Repository\Adapter\ImmutableWriter; use Dotenv\Repository\Adapter\MultiReader; use Dotenv\Repository\Adapter\MultiWriter; use Dotenv\Repository\Adapter\ReaderInterface; use Dotenv\Repository\Adapter\ServerConstAdapter; use Dotenv\Repository\Adapter\WriterInterface; use InvalidArgumentException; use PhpOption\Some; use ReflectionClass; final class RepositoryBuilder { /** * The set of default adapters. */ private const DEFAULT_ADAPTERS = [ ServerConstAdapter::class, EnvConstAdapter::class, ]; /** * The set of readers to use. * * @var \Dotenv\Repository\Adapter\ReaderInterface[] */ private $readers; /** * The set of writers to use. * * @var \Dotenv\Repository\Adapter\WriterInterface[] */ private $writers; /** * Are we immutable? * * @var bool */ private $immutable; /** * The variable name allow list. * * @var string[]|null */ private $allowList; /** * Create a new repository builder instance. * * @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers * @param \Dotenv\Repository\Adapter\WriterInterface[] $writers * @param bool $immutable * @param string[]|null $allowList * * @return void */ private function __construct(array $readers = [], array $writers = [], bool $immutable = false, array $allowList = null) { $this->readers = $readers; $this->writers = $writers; $this->immutable = $immutable; $this->allowList = $allowList; } /** * Create a new repository builder instance with no adapters added. * * @return \Dotenv\Repository\RepositoryBuilder */ public static function createWithNoAdapters() { return new self(); } /** * Create a new repository builder instance with the default adapters added. * * @return \Dotenv\Repository\RepositoryBuilder */ public static function createWithDefaultAdapters() { $adapters = \iterator_to_array(self::defaultAdapters()); return new self($adapters, $adapters); } /** * Return the array of default adapters. * * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> */ private static function defaultAdapters() { foreach (self::DEFAULT_ADAPTERS as $adapter) { $instance = $adapter::create(); if ($instance->isDefined()) { yield $instance->get(); } } } /** * Determine if the given name if of an adapaterclass. * * @param string $name * * @return bool */ private static function isAnAdapterClass(string $name) { if (!\class_exists($name)) { return false; } return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); } /** * Creates a repository builder with the given reader added. * * Accepts either a reader instance, or a class-string for an adapter. If * the adapter is not supported, then we silently skip adding it. * * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader * * @throws \InvalidArgumentException * * @return \Dotenv\Repository\RepositoryBuilder */ public function addReader($reader) { if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { throw new InvalidArgumentException( \sprintf( 'Expected either an instance of %s or a class-string implementing %s', ReaderInterface::class, AdapterInterface::class ) ); } $optional = Some::create($reader)->flatMap(static function ($reader) { return \is_string($reader) ? $reader::create() : Some::create($reader); }); $readers = \array_merge($this->readers, \iterator_to_array($optional)); return new self($readers, $this->writers, $this->immutable, $this->allowList); } /** * Creates a repository builder with the given writer added. * * Accepts either a writer instance, or a class-string for an adapter. If * the adapter is not supported, then we silently skip adding it. * * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer * * @throws \InvalidArgumentException * * @return \Dotenv\Repository\RepositoryBuilder */ public function addWriter($writer) { if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { throw new InvalidArgumentException( \sprintf( 'Expected either an instance of %s or a class-string implementing %s', WriterInterface::class, AdapterInterface::class ) ); } $optional = Some::create($writer)->flatMap(static function ($writer) { return \is_string($writer) ? $writer::create() : Some::create($writer); }); $writers = \array_merge($this->writers, \iterator_to_array($optional)); return new self($this->readers, $writers, $this->immutable, $this->allowList); } /** * Creates a repository builder with the given adapter added. * * Accepts either an adapter instance, or a class-string for an adapter. If * the adapter is not supported, then we silently skip adding it. We will * add the adapter as both a reader and a writer. * * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter * * @throws \InvalidArgumentException * * @return \Dotenv\Repository\RepositoryBuilder */ public function addAdapter($adapter) { if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { throw new InvalidArgumentException( \sprintf( 'Expected either an instance of %s or a class-string implementing %s', WriterInterface::class, AdapterInterface::class ) ); } $optional = Some::create($adapter)->flatMap(static function ($adapter) { return \is_string($adapter) ? $adapter::create() : Some::create($adapter); }); $readers = \array_merge($this->readers, \iterator_to_array($optional)); $writers = \array_merge($this->writers, \iterator_to_array($optional)); return new self($readers, $writers, $this->immutable, $this->allowList); } /** * Creates a repository builder with mutability enabled. * * @return \Dotenv\Repository\RepositoryBuilder */ public function immutable() { return new self($this->readers, $this->writers, true, $this->allowList); } /** * Creates a repository builder with the given allow list. * * @param string[]|null $allowList * * @return \Dotenv\Repository\RepositoryBuilder */ public function allowList(array $allowList = null) { return new self($this->readers, $this->writers, $this->immutable, $allowList); } /** * Creates a new repository instance. * * @return \Dotenv\Repository\RepositoryInterface */ public function make() { $reader = new MultiReader($this->readers); $writer = new MultiWriter($this->writers); if ($this->immutable) { $writer = new ImmutableWriter($writer, $reader); } if ($this->allowList !== null) { $writer = new GuardedWriter($writer, $this->allowList); } return new AdapterRepository($reader, $writer); } } Util/Regex.php 0000644 00000005573 15025063706 0007262 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Util; use GrahamCampbell\ResultType\Error; use GrahamCampbell\ResultType\Success; /** * @internal */ final class Regex { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Perform a preg match, wrapping up the result. * * @param string $pattern * @param string $subject * * @return \GrahamCampbell\ResultType\Result<bool,string> */ public static function matches(string $pattern, string $subject) { return self::pregAndWrap(static function (string $subject) use ($pattern) { return @\preg_match($pattern, $subject) === 1; }, $subject); } /** * Perform a preg match all, wrapping up the result. * * @param string $pattern * @param string $subject * * @return \GrahamCampbell\ResultType\Result<int,string> */ public static function occurences(string $pattern, string $subject) { return self::pregAndWrap(static function (string $subject) use ($pattern) { return (int) @\preg_match_all($pattern, $subject); }, $subject); } /** * Perform a preg replace callback, wrapping up the result. * * @param string $pattern * @param callable $callback * @param string $subject * @param int|null $limit * * @return \GrahamCampbell\ResultType\Result<string,string> */ public static function replaceCallback(string $pattern, callable $callback, string $subject, int $limit = null) { return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); }, $subject); } /** * Perform a preg split, wrapping up the result. * * @param string $pattern * @param string $subject * * @return \GrahamCampbell\ResultType\Result<string[],string> */ public static function split(string $pattern, string $subject) { return self::pregAndWrap(static function (string $subject) use ($pattern) { /** @var string[] */ return (array) @\preg_split($pattern, $subject); }, $subject); } /** * Perform a preg operation, wrapping up the result. * * @template V * * @param callable(string):V $operation * @param string $subject * * @return \GrahamCampbell\ResultType\Result<V,string> */ private static function pregAndWrap(callable $operation, string $subject) { $result = $operation($subject); if (\preg_last_error() !== \PREG_NO_ERROR) { return Error::create(\preg_last_error_msg()); } return Success::create($result); } } Util/Str.php 0000644 00000004735 15025063706 0006757 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Util; use GrahamCampbell\ResultType\Error; use GrahamCampbell\ResultType\Success; use PhpOption\Option; /** * @internal */ final class Str { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Convert a string to UTF-8 from the given encoding. * * @param string $input * @param string|null $encoding * * @return \GrahamCampbell\ResultType\Result<string,string> */ public static function utf8(string $input, string $encoding = null) { if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { /** @var \GrahamCampbell\ResultType\Result<string,string> */ return Error::create( \sprintf('Illegal character encoding [%s] specified.', $encoding) ); } $converted = $encoding === null ? @\mb_convert_encoding($input, 'UTF-8') : @\mb_convert_encoding($input, 'UTF-8', $encoding); /** * this is for support UTF-8 with BOM encoding * @see https://en.wikipedia.org/wiki/Byte_order_mark * @see https://github.com/vlucas/phpdotenv/issues/500 */ if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { $converted = \substr($converted, 3); } /** @var \GrahamCampbell\ResultType\Result<string,string> */ return Success::create($converted); } /** * Search for a given substring of the input. * * @param string $haystack * @param string $needle * * @return \PhpOption\Option<int> */ public static function pos(string $haystack, string $needle) { /** @var \PhpOption\Option<int> */ return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); } /** * Grab the specified substring of the input. * * @param string $input * @param int $start * @param int|null $length * * @return string */ public static function substr(string $input, int $start, int $length = null) { return \mb_substr($input, $start, $length, 'UTF-8'); } /** * Compute the length of the given string. * * @param string $input * * @return int */ public static function len(string $input) { return \mb_strlen($input, 'UTF-8'); } } Exception/InvalidFileException.php 0000644 00000000310 15025063706 0013256 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Exception; use InvalidArgumentException; final class InvalidFileException extends InvalidArgumentException implements ExceptionInterface { // } Exception/InvalidEncodingException.php 0000644 00000000314 15025063706 0014131 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Exception; use InvalidArgumentException; final class InvalidEncodingException extends InvalidArgumentException implements ExceptionInterface { // } Exception/ValidationException.php 0000644 00000000267 15025063706 0013175 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Exception; use RuntimeException; final class ValidationException extends RuntimeException implements ExceptionInterface { // } Exception/ExceptionInterface.php 0000644 00000000210 15025063706 0012767 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Throwable; interface ExceptionInterface extends Throwable { } Exception/InvalidPathException.php 0000644 00000000310 15025063706 0013273 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Exception; use InvalidArgumentException; final class InvalidPathException extends InvalidArgumentException implements ExceptionInterface { // } Validator.php 0000644 00000012177 15025063706 0007216 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv; use Dotenv\Exception\ValidationException; use Dotenv\Repository\RepositoryInterface; use Dotenv\Util\Regex; use Dotenv\Util\Str; class Validator { /** * The environment repository instance. * * @var \Dotenv\Repository\RepositoryInterface */ private $repository; /** * The variables to validate. * * @var string[] */ private $variables; /** * Create a new validator instance. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param string[] $variables * * @throws \Dotenv\Exception\ValidationException * * @return void */ public function __construct(RepositoryInterface $repository, array $variables) { $this->repository = $repository; $this->variables = $variables; } /** * Assert that each variable is present. * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function required() { return $this->assert( static function (?string $value) { return $value !== null; }, 'is missing' ); } /** * Assert that each variable is not empty. * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function notEmpty() { return $this->assertNullable( static function (string $value) { return Str::len(\trim($value)) > 0; }, 'is empty' ); } /** * Assert that each specified variable is an integer. * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function isInteger() { return $this->assertNullable( static function (string $value) { return \ctype_digit($value); }, 'is not an integer' ); } /** * Assert that each specified variable is a boolean. * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function isBoolean() { return $this->assertNullable( static function (string $value) { if ($value === '') { return false; } return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; }, 'is not a boolean' ); } /** * Assert that each variable is amongst the given choices. * * @param string[] $choices * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function allowedValues(array $choices) { return $this->assertNullable( static function (string $value) use ($choices) { return \in_array($value, $choices, true); }, \sprintf('is not one of [%s]', \implode(', ', $choices)) ); } /** * Assert that each variable matches the given regular expression. * * @param string $regex * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function allowedRegexValues(string $regex) { return $this->assertNullable( static function (string $value) use ($regex) { return Regex::matches($regex, $value)->success()->getOrElse(false); }, \sprintf('does not match "%s"', $regex) ); } /** * Assert that the callback returns true for each variable. * * @param callable(?string):bool $callback * @param string $message * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function assert(callable $callback, string $message) { $failing = []; foreach ($this->variables as $variable) { if ($callback($this->repository->get($variable)) === false) { $failing[] = \sprintf('%s %s', $variable, $message); } } if (\count($failing) > 0) { throw new ValidationException(\sprintf( 'One or more environment variables failed assertions: %s.', \implode(', ', $failing) )); } return $this; } /** * Assert that the callback returns true for each variable. * * Skip checking null variable values. * * @param callable(string):bool $callback * @param string $message * * @throws \Dotenv\Exception\ValidationException * * @return \Dotenv\Validator */ public function assertNullable(callable $callback, string $message) { return $this->assert( static function (?string $value) use ($callback) { if ($value === null) { return true; } return $callback($value); }, $message ); } } Loader/LoaderInterface.php 0000644 00000000711 15025063706 0011515 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Loader; use Dotenv\Repository\RepositoryInterface; interface LoaderInterface { /** * Load the given entries into the repository. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param \Dotenv\Parser\Entry[] $entries * * @return array<string,string|null> */ public function load(RepositoryInterface $repository, array $entries); } Loader/Resolver.php 0000644 00000003330 15025063706 0010267 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Loader; use Dotenv\Parser\Value; use Dotenv\Repository\RepositoryInterface; use Dotenv\Util\Regex; use Dotenv\Util\Str; use PhpOption\Option; final class Resolver { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Resolve the nested variables in the given value. * * Replaces ${varname} patterns in the allowed positions in the variable * value by an existing environment variable. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param \Dotenv\Parser\Value $value * * @return string */ public static function resolve(RepositoryInterface $repository, Value $value) { return \array_reduce($value->getVars(), static function (string $s, int $i) use ($repository) { return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); }, $value->getChars()); } /** * Resolve a single nested variable. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param string $str * * @return string */ private static function resolveVariable(RepositoryInterface $repository, string $str) { return Regex::replaceCallback( '/\A\${([a-zA-Z0-9_.]+)}/', static function (array $matches) use ($repository) { return Option::fromValue($repository->get($matches[1])) ->getOrElse($matches[0]); }, $str, 1 )->success()->getOrElse($str); } } Loader/Loader.php 0000644 00000002642 15025063706 0007701 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Loader; use Dotenv\Parser\Entry; use Dotenv\Parser\Value; use Dotenv\Repository\RepositoryInterface; final class Loader implements LoaderInterface { /** * Load the given entries into the repository. * * We'll substitute any nested variables, and send each variable to the * repository, with the effect of actually mutating the environment. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param \Dotenv\Parser\Entry[] $entries * * @return array<string,string|null> */ public function load(RepositoryInterface $repository, array $entries) { return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { $name = $entry->getName(); $value = $entry->getValue()->map(static function (Value $value) use ($repository) { return Resolver::resolve($repository, $value); }); if ($value->isDefined()) { $inner = $value->get(); if ($repository->set($name, $inner)) { return \array_merge($vars, [$name => $inner]); } } else { if ($repository->clear($name)) { return \array_merge($vars, [$name => null]); } } return $vars; }, []); } } Dotenv.php 0000644 00000020102 15025063706 0006513 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv; use Dotenv\Exception\InvalidPathException; use Dotenv\Loader\Loader; use Dotenv\Loader\LoaderInterface; use Dotenv\Parser\Parser; use Dotenv\Parser\ParserInterface; use Dotenv\Repository\Adapter\ArrayAdapter; use Dotenv\Repository\Adapter\PutenvAdapter; use Dotenv\Repository\RepositoryBuilder; use Dotenv\Repository\RepositoryInterface; use Dotenv\Store\StoreBuilder; use Dotenv\Store\StoreInterface; use Dotenv\Store\StringStore; class Dotenv { /** * The store instance. * * @var \Dotenv\Store\StoreInterface */ private $store; /** * The parser instance. * * @var \Dotenv\Parser\ParserInterface */ private $parser; /** * The loader instance. * * @var \Dotenv\Loader\LoaderInterface */ private $loader; /** * The repository instance. * * @var \Dotenv\Repository\RepositoryInterface */ private $repository; /** * Create a new dotenv instance. * * @param \Dotenv\Store\StoreInterface $store * @param \Dotenv\Parser\ParserInterface $parser * @param \Dotenv\Loader\LoaderInterface $loader * @param \Dotenv\Repository\RepositoryInterface $repository * * @return void */ public function __construct( StoreInterface $store, ParserInterface $parser, LoaderInterface $loader, RepositoryInterface $repository ) { $this->store = $store; $this->parser = $parser; $this->loader = $loader; $this->repository = $repository; } /** * Create a new dotenv instance. * * @param \Dotenv\Repository\RepositoryInterface $repository * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); foreach ((array) $paths as $path) { $builder = $builder->addPath($path); } foreach ((array) $names as $name) { $builder = $builder->addName($name); } if ($shortCircuit) { $builder = $builder->shortCircuit(); } return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); } /** * Create a new mutable dotenv instance with default repository. * * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function createMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** * Create a new mutable dotenv instance with default repository with the putenv adapter. * * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithDefaultAdapters() ->addAdapter(PutenvAdapter::class) ->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** * Create a new immutable dotenv instance with default repository. * * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** * Create a new immutable dotenv instance with default repository with the putenv adapter. * * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithDefaultAdapters() ->addAdapter(PutenvAdapter::class) ->immutable() ->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** * Create a new dotenv instance with an array backed repository. * * @param string|string[] $paths * @param string|string[]|null $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return \Dotenv\Dotenv */ public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** * Parse the given content and resolve nested variables. * * This method behaves just like load(), only without mutating your actual * environment. We do this by using an array backed repository. * * @param string $content * * @throws \Dotenv\Exception\InvalidFileException * * @return array<string,string|null> */ public static function parse(string $content) { $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); return $phpdotenv->load(); } /** * Read and load environment file(s). * * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException * * @return array<string,string|null> */ public function load() { $entries = $this->parser->parse($this->store->read()); return $this->loader->load($this->repository, $entries); } /** * Read and load environment file(s), silently failing if no files can be read. * * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException * * @return array<string,string|null> */ public function safeLoad() { try { return $this->load(); } catch (InvalidPathException $e) { // suppressing exception return []; } } /** * Required ensures that the specified variables exist, and returns a new validator object. * * @param string|string[] $variables * * @return \Dotenv\Validator */ public function required($variables) { return (new Validator($this->repository, (array) $variables))->required(); } /** * Returns a new validator object that won't check if the specified variables exist. * * @param string|string[] $variables * * @return \Dotenv\Validator */ public function ifPresent($variables) { return new Validator($this->repository, (array) $variables); } } Store/File/Paths.php 0000644 00000001354 15025063706 0010316 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store\File; /** * @internal */ final class Paths { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Returns the full paths to the files. * * @param string[] $paths * @param string[] $names * * @return string[] */ public static function filePaths(array $paths, array $names) { $files = []; foreach ($paths as $path) { foreach ($names as $name) { $files[] = \rtrim($path, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.$name; } } return $files; } } Store/File/Reader.php 0000644 00000004065 15025063706 0010443 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store\File; use Dotenv\Exception\InvalidEncodingException; use Dotenv\Util\Str; use PhpOption\Option; /** * @internal */ final class Reader { /** * This class is a singleton. * * @codeCoverageIgnore * * @return void */ private function __construct() { // } /** * Read the file(s), and return their raw content. * * We provide the file path as the key, and its content as the value. If * short circuit mode is enabled, then the returned array with have length * at most one. File paths that couldn't be read are omitted entirely. * * @param string[] $filePaths * @param bool $shortCircuit * @param string|null $fileEncoding * * @throws \Dotenv\Exception\InvalidEncodingException * * @return array<string,string> */ public static function read(array $filePaths, bool $shortCircuit = true, string $fileEncoding = null) { $output = []; foreach ($filePaths as $filePath) { $content = self::readFromFile($filePath, $fileEncoding); if ($content->isDefined()) { $output[$filePath] = $content->get(); if ($shortCircuit) { break; } } } return $output; } /** * Read the given file. * * @param string $path * @param string|null $encoding * * @throws \Dotenv\Exception\InvalidEncodingException * * @return \PhpOption\Option<string> */ private static function readFromFile(string $path, string $encoding = null) { /** @var Option<string> */ $content = Option::fromValue(@\file_get_contents($path), false); return $content->flatMap(static function (string $content) use ($encoding) { return Str::utf8($content, $encoding)->mapError(static function (string $error) { throw new InvalidEncodingException($error); })->success(); }); } } Store/StoreBuilder.php 0000644 00000006142 15025063706 0010763 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store; use Dotenv\Store\File\Paths; final class StoreBuilder { /** * The of default name. */ private const DEFAULT_NAME = '.env'; /** * The paths to search within. * * @var string[] */ private $paths; /** * The file names to search for. * * @var string[] */ private $names; /** * Should file loading short circuit? * * @var bool */ private $shortCircuit; /** * The file encoding. * * @var string|null */ private $fileEncoding; /** * Create a new store builder instance. * * @param string[] $paths * @param string[] $names * @param bool $shortCircuit * @param string|null $fileEncoding * * @return void */ private function __construct(array $paths = [], array $names = [], bool $shortCircuit = false, string $fileEncoding = null) { $this->paths = $paths; $this->names = $names; $this->shortCircuit = $shortCircuit; $this->fileEncoding = $fileEncoding; } /** * Create a new store builder instance with no names. * * @return \Dotenv\Store\StoreBuilder */ public static function createWithNoNames() { return new self(); } /** * Create a new store builder instance with the default name. * * @return \Dotenv\Store\StoreBuilder */ public static function createWithDefaultName() { return new self([], [self::DEFAULT_NAME]); } /** * Creates a store builder with the given path added. * * @param string $path * * @return \Dotenv\Store\StoreBuilder */ public function addPath(string $path) { return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); } /** * Creates a store builder with the given name added. * * @param string $name * * @return \Dotenv\Store\StoreBuilder */ public function addName(string $name) { return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); } /** * Creates a store builder with short circuit mode enabled. * * @return \Dotenv\Store\StoreBuilder */ public function shortCircuit() { return new self($this->paths, $this->names, true, $this->fileEncoding); } /** * Creates a store builder with the specified file encoding. * * @param string|null $fileEncoding * * @return \Dotenv\Store\StoreBuilder */ public function fileEncoding(string $fileEncoding = null) { return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); } /** * Creates a new store instance. * * @return \Dotenv\Store\StoreInterface */ public function make() { return new FileStore( Paths::filePaths($this->paths, $this->names), $this->shortCircuit, $this->fileEncoding ); } } Store/FileStore.php 0000644 00000003217 15025063706 0010254 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store; use Dotenv\Exception\InvalidPathException; use Dotenv\Store\File\Reader; final class FileStore implements StoreInterface { /** * The file paths. * * @var string[] */ private $filePaths; /** * Should file loading short circuit? * * @var bool */ private $shortCircuit; /** * The file encoding. * * @var string|null */ private $fileEncoding; /** * Create a new file store instance. * * @param string[] $filePaths * @param bool $shortCircuit * @param string|null $fileEncoding * * @return void */ public function __construct(array $filePaths, bool $shortCircuit, string $fileEncoding = null) { $this->filePaths = $filePaths; $this->shortCircuit = $shortCircuit; $this->fileEncoding = $fileEncoding; } /** * Read the content of the environment file(s). * * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException * * @return string */ public function read() { if ($this->filePaths === []) { throw new InvalidPathException('At least one environment file path must be provided.'); } $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); if (\count($contents) > 0) { return \implode("\n", $contents); } throw new InvalidPathException( \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) ); } } Store/StoreInterface.php 0000644 00000000474 15025063706 0011277 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store; interface StoreInterface { /** * Read the content of the environment file(s). * * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException * * @return string */ public function read(); } Store/StringStore.php 0000644 00000001115 15025063706 0010636 0 ustar 00 <?php declare(strict_types=1); namespace Dotenv\Store; final class StringStore implements StoreInterface { /** * The file content. * * @var string */ private $content; /** * Create a new string store instance. * * @param string $content * * @return void */ public function __construct(string $content) { $this->content = $content; } /** * Read the content of the environment file(s). * * @return string */ public function read() { return $this->content; } } PhpOption/None.php 0000644 00000004722 15025064030 0010074 0 ustar 00 <?php /* * Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace PhpOption; use EmptyIterator; /** * @extends Option<mixed> */ final class None extends Option { /** @var None|null */ private static $instance; /** * @return None */ public static function create(): self { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function get() { throw new \RuntimeException('None has no value.'); } public function getOrCall($callable) { return $callable(); } public function getOrElse($default) { return $default; } public function getOrThrow(\Exception $ex) { throw $ex; } public function isEmpty(): bool { return true; } public function isDefined(): bool { return false; } public function orElse(Option $else) { return $else; } public function ifDefined($callable) { // Just do nothing in that case. } public function forAll($callable) { return $this; } public function map($callable) { return $this; } public function flatMap($callable) { return $this; } public function filter($callable) { return $this; } public function filterNot($callable) { return $this; } public function select($value) { return $this; } public function reject($value) { return $this; } public function getIterator(): EmptyIterator { return new EmptyIterator(); } public function foldLeft($initialValue, $callable) { return $initialValue; } public function foldRight($initialValue, $callable) { return $initialValue; } private function __construct() { } } PhpOption/Some.php 0000644 00000006367 15025064030 0010107 0 ustar 00 <?php /* * Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace PhpOption; use ArrayIterator; /** * @template T * * @extends Option<T> */ final class Some extends Option { /** @var T */ private $value; /** * @param T $value */ public function __construct($value) { $this->value = $value; } /** * @template U * * @param U $value * * @return Some<U> */ public static function create($value): self { return new self($value); } public function isDefined(): bool { return true; } public function isEmpty(): bool { return false; } public function get() { return $this->value; } public function getOrElse($default) { return $this->value; } public function getOrCall($callable) { return $this->value; } public function getOrThrow(\Exception $ex) { return $this->value; } public function orElse(Option $else) { return $this; } public function ifDefined($callable) { $this->forAll($callable); } public function forAll($callable) { $callable($this->value); return $this; } public function map($callable) { return new self($callable($this->value)); } public function flatMap($callable) { /** @var mixed */ $rs = $callable($this->value); if (!$rs instanceof Option) { throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); } return $rs; } public function filter($callable) { if (true === $callable($this->value)) { return $this; } return None::create(); } public function filterNot($callable) { if (false === $callable($this->value)) { return $this; } return None::create(); } public function select($value) { if ($this->value === $value) { return $this; } return None::create(); } public function reject($value) { if ($this->value === $value) { return None::create(); } return $this; } /** * @return ArrayIterator<int, T> */ public function getIterator(): ArrayIterator { return new ArrayIterator([$this->value]); } public function foldLeft($initialValue, $callable) { return $callable($initialValue, $this->value); } public function foldRight($initialValue, $callable) { return $callable($this->value, $initialValue); } } PhpOption/LazyOption.php 0000644 00000007762 15025064030 0011314 0 ustar 00 <?php /* * Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace PhpOption; use Traversable; /** * @template T * * @extends Option<T> */ final class LazyOption extends Option { /** @var callable(mixed...):(Option<T>) */ private $callback; /** @var array<int, mixed> */ private $arguments; /** @var Option<T>|null */ private $option; /** * @template S * @param callable(mixed...):(Option<S>) $callback * @param array<int, mixed> $arguments * * @return LazyOption<S> */ public static function create($callback, array $arguments = []): self { return new self($callback, $arguments); } /** * @param callable(mixed...):(Option<T>) $callback * @param array<int, mixed> $arguments */ public function __construct($callback, array $arguments = []) { if (!is_callable($callback)) { throw new \InvalidArgumentException('Invalid callback given'); } $this->callback = $callback; $this->arguments = $arguments; } public function isDefined(): bool { return $this->option()->isDefined(); } public function isEmpty(): bool { return $this->option()->isEmpty(); } public function get() { return $this->option()->get(); } public function getOrElse($default) { return $this->option()->getOrElse($default); } public function getOrCall($callable) { return $this->option()->getOrCall($callable); } public function getOrThrow(\Exception $ex) { return $this->option()->getOrThrow($ex); } public function orElse(Option $else) { return $this->option()->orElse($else); } public function ifDefined($callable) { $this->option()->forAll($callable); } public function forAll($callable) { return $this->option()->forAll($callable); } public function map($callable) { return $this->option()->map($callable); } public function flatMap($callable) { return $this->option()->flatMap($callable); } public function filter($callable) { return $this->option()->filter($callable); } public function filterNot($callable) { return $this->option()->filterNot($callable); } public function select($value) { return $this->option()->select($value); } public function reject($value) { return $this->option()->reject($value); } /** * @return Traversable<T> */ public function getIterator(): Traversable { return $this->option()->getIterator(); } public function foldLeft($initialValue, $callable) { return $this->option()->foldLeft($initialValue, $callable); } public function foldRight($initialValue, $callable) { return $this->option()->foldRight($initialValue, $callable); } /** * @return Option<T> */ private function option(): Option { if (null === $this->option) { /** @var mixed */ $option = call_user_func_array($this->callback, $this->arguments); if ($option instanceof Option) { $this->option = $option; } else { throw new \RuntimeException(sprintf('Expected instance of %s', Option::class)); } } return $this->option; } } PhpOption/Option.php 0000644 00000032034 15025064030 0010442 0 ustar 00 <?php /* * Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace PhpOption; use ArrayAccess; use IteratorAggregate; /** * @template T * * @implements IteratorAggregate<T> */ abstract class Option implements IteratorAggregate { /** * Creates an option given a return value. * * This is intended for consuming existing APIs and allows you to easily * convert them to an option. By default, we treat ``null`` as the None * case, and everything else as Some. * * @template S * * @param S $value The actual return value. * @param S $noneValue The value which should be considered "None"; null by * default. * * @return Option<S> */ public static function fromValue($value, $noneValue = null) { if ($value === $noneValue) { return None::create(); } return new Some($value); } /** * Creates an option from an array's value. * * If the key does not exist in the array, the array is not actually an * array, or the array's value at the given key is null, None is returned. * Otherwise, Some is returned wrapping the value at the given key. * * @template S * * @param array<string|int,S>|ArrayAccess<string|int,S>|null $array A potential array or \ArrayAccess value. * @param string $key The key to check. * * @return Option<S> */ public static function fromArraysValue($array, $key) { if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) { return None::create(); } return new Some($array[$key]); } /** * Creates a lazy-option with the given callback. * * This is also a helper constructor for lazy-consuming existing APIs where * the return value is not yet an option. By default, we treat ``null`` as * None case, and everything else as Some. * * @template S * * @param callable $callback The callback to evaluate. * @param array $arguments The arguments for the callback. * @param S $noneValue The value which should be considered "None"; * null by default. * * @return LazyOption<S> */ public static function fromReturn($callback, array $arguments = [], $noneValue = null) { return new LazyOption(static function () use ($callback, $arguments, $noneValue) { /** @var mixed */ $return = call_user_func_array($callback, $arguments); if ($return === $noneValue) { return None::create(); } return new Some($return); }); } /** * Option factory, which creates new option based on passed value. * * If value is already an option, it simply returns. If value is callable, * LazyOption with passed callback created and returned. If Option * returned from callback, it returns directly. On other case value passed * to Option::fromValue() method. * * @template S * * @param Option<S>|callable|S $value * @param S $noneValue Used when $value is mixed or * callable, for None-check. * * @return Option<S>|LazyOption<S> */ public static function ensure($value, $noneValue = null) { if ($value instanceof self) { return $value; } elseif (is_callable($value)) { return new LazyOption(static function () use ($value, $noneValue) { /** @var mixed */ $return = $value(); if ($return instanceof self) { return $return; } else { return self::fromValue($return, $noneValue); } }); } else { return self::fromValue($value, $noneValue); } } /** * Lift a function so that it accepts Option as parameters. * * We return a new closure that wraps the original callback. If any of the * parameters passed to the lifted function is empty, the function will * return a value of None. Otherwise, we will pass all parameters to the * original callback and return the value inside a new Option, unless an * Option is returned from the function, in which case, we use that. * * @template S * * @param callable $callback * @param mixed $noneValue * * @return callable */ public static function lift($callback, $noneValue = null) { return static function () use ($callback, $noneValue) { /** @var array<int, mixed> */ $args = func_get_args(); $reduced_args = array_reduce( $args, /** @param bool $status */ static function ($status, self $o) { return $o->isEmpty() ? true : $status; }, false ); // if at least one parameter is empty, return None if ($reduced_args) { return None::create(); } $args = array_map( /** @return T */ static function (self $o) { // it is safe to do so because the fold above checked // that all arguments are of type Some /** @var T */ return $o->get(); }, $args ); return self::ensure(call_user_func_array($callback, $args), $noneValue); }; } /** * Returns the value if available, or throws an exception otherwise. * * @throws \RuntimeException If value is not available. * * @return T */ abstract public function get(); /** * Returns the value if available, or the default value if not. * * @template S * * @param S $default * * @return T|S */ abstract public function getOrElse($default); /** * Returns the value if available, or the results of the callable. * * This is preferable over ``getOrElse`` if the computation of the default * value is expensive. * * @template S * * @param callable():S $callable * * @return T|S */ abstract public function getOrCall($callable); /** * Returns the value if available, or throws the passed exception. * * @param \Exception $ex * * @return T */ abstract public function getOrThrow(\Exception $ex); /** * Returns true if no value is available, false otherwise. * * @return bool */ abstract public function isEmpty(); /** * Returns true if a value is available, false otherwise. * * @return bool */ abstract public function isDefined(); /** * Returns this option if non-empty, or the passed option otherwise. * * This can be used to try multiple alternatives, and is especially useful * with lazy evaluating options: * * ```php * $repo->findSomething() * ->orElse(new LazyOption(array($repo, 'findSomethingElse'))) * ->orElse(new LazyOption(array($repo, 'createSomething'))); * ``` * * @param Option<T> $else * * @return Option<T> */ abstract public function orElse(self $else); /** * This is similar to map() below except that the return value has no meaning; * the passed callable is simply executed if the option is non-empty, and * ignored if the option is empty. * * In all cases, the return value of the callable is discarded. * * ```php * $comment->getMaybeFile()->ifDefined(function($file) { * // Do something with $file here. * }); * ``` * * If you're looking for something like ``ifEmpty``, you can use ``getOrCall`` * and ``getOrElse`` in these cases. * * @deprecated Use forAll() instead. * * @param callable(T):mixed $callable * * @return void */ abstract public function ifDefined($callable); /** * This is similar to map() except that the return value of the callable has no meaning. * * The passed callable is simply executed if the option is non-empty, and ignored if the * option is empty. This method is preferred for callables with side-effects, while map() * is intended for callables without side-effects. * * @param callable(T):mixed $callable * * @return Option<T> */ abstract public function forAll($callable); /** * Applies the callable to the value of the option if it is non-empty, * and returns the return value of the callable wrapped in Some(). * * If the option is empty, then the callable is not applied. * * ```php * (new Some("foo"))->map('strtoupper')->get(); // "FOO" * ``` * * @template S * * @param callable(T):S $callable * * @return Option<S> */ abstract public function map($callable); /** * Applies the callable to the value of the option if it is non-empty, and * returns the return value of the callable directly. * * In contrast to ``map``, the return value of the callable is expected to * be an Option itself; it is not automatically wrapped in Some(). * * @template S * * @param callable(T):Option<S> $callable must return an Option * * @return Option<S> */ abstract public function flatMap($callable); /** * If the option is empty, it is returned immediately without applying the callable. * * If the option is non-empty, the callable is applied, and if it returns true, * the option itself is returned; otherwise, None is returned. * * @param callable(T):bool $callable * * @return Option<T> */ abstract public function filter($callable); /** * If the option is empty, it is returned immediately without applying the callable. * * If the option is non-empty, the callable is applied, and if it returns false, * the option itself is returned; otherwise, None is returned. * * @param callable(T):bool $callable * * @return Option<T> */ abstract public function filterNot($callable); /** * If the option is empty, it is returned immediately. * * If the option is non-empty, and its value does not equal the passed value * (via a shallow comparison ===), then None is returned. Otherwise, the * Option is returned. * * In other words, this will filter all but the passed value. * * @param T $value * * @return Option<T> */ abstract public function select($value); /** * If the option is empty, it is returned immediately. * * If the option is non-empty, and its value does equal the passed value (via * a shallow comparison ===), then None is returned; otherwise, the Option is * returned. * * In other words, this will let all values through except the passed value. * * @param T $value * * @return Option<T> */ abstract public function reject($value); /** * Binary operator for the initial value and the option's value. * * If empty, the initial value is returned. If non-empty, the callable * receives the initial value and the option's value as arguments. * * ```php * * $some = new Some(5); * $none = None::create(); * $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6) * $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1) * * // This can be used instead of something like the following: * $option = Option::fromValue($integerOrNull); * $result = 1; * if ( ! $option->isEmpty()) { * $result += $option->get(); * } * ``` * * @template S * * @param S $initialValue * @param callable(S, T):S $callable * * @return S */ abstract public function foldLeft($initialValue, $callable); /** * foldLeft() but with reversed arguments for the callable. * * @template S * * @param S $initialValue * @param callable(T, S):S $callable * * @return S */ abstract public function foldRight($initialValue, $callable); } autoloader.php 0000644 00000002154 15025064345 0007422 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; /** * A simple PSR-4 spec auto loader to allow json-machine to function the same as if it were loaded via Composer. * * To use this just include this file in your script and the JsonMachine namespace will be made available * * Usage: spl_autoload_register(require '/path/to/json-machine/src/autoloader.php'); * * See: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md * * @param string $class the fully-qualified class name * * @return void */ class Autoloading { static public function autoloader($class) { $prefix = 'JsonMachine\\'; $baseDir = __DIR__.DIRECTORY_SEPARATOR; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $classWithoutPrefix = substr($class, $len); $file = $baseDir.str_replace('\\', '/', $classWithoutPrefix).'.php'; if (file_exists($file)) { require $file; } } } // @codeCoverageIgnoreStart return [Autoloading::class, 'autoloader']; // @codeCoverageIgnoreEnd Items.php 0000644 00000006122 15025064345 0006343 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use JsonMachine\Exception\InvalidArgumentException; use JsonMachine\JsonDecoder\ExtJsonDecoder; use JsonMachine\JsonDecoder\ItemDecoder; /** * Entry-point facade for JSON Machine. */ final class Items implements \IteratorAggregate, PositionAware { /** * @var iterable */ private $chunks; /** * @var string */ private $jsonPointer; /** * @var ItemDecoder|null */ private $jsonDecoder; /** * @var Parser */ private $parser; /** * @var bool */ private $debugEnabled; /** * @param iterable $bytesIterator * * @throws InvalidArgumentException */ public function __construct($bytesIterator, array $options = []) { $options = new ItemsOptions($options); $this->chunks = $bytesIterator; $this->jsonPointer = $options['pointer']; $this->jsonDecoder = $options['decoder']; $this->debugEnabled = $options['debug']; if ($this->debugEnabled) { $tokensClass = TokensWithDebugging::class; } else { $tokensClass = Tokens::class; } $this->parser = new Parser( new $tokensClass( $this->chunks ), $this->jsonPointer, $this->jsonDecoder ?: new ExtJsonDecoder() ); } /** * @param string $string * * @return self * * @throws InvalidArgumentException */ public static function fromString($string, array $options = []) { return new self(new StringChunks($string), $options); } /** * @param string $file * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromFile($file, array $options = []) { return new self(new FileChunks($file), $options); } /** * @param resource $stream * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromStream($stream, array $options = []) { return new self(new StreamChunks($stream), $options); } /** * @param iterable $iterable * * @return self * * @throws Exception\InvalidArgumentException */ public static function fromIterable($iterable, array $options = []) { return new self($iterable, $options); } #[\ReturnTypeWillChange] public function getIterator() { return $this->parser->getIterator(); } public function getPosition() { return $this->parser->getPosition(); } public function getJsonPointers(): array { return $this->parser->getJsonPointers(); } public function getCurrentJsonPointer(): string { return $this->parser->getCurrentJsonPointer(); } public function getMatchedJsonPointer(): string { return $this->parser->getMatchedJsonPointer(); } /** * @return bool */ public function isDebugEnabled() { return $this->debugEnabled; } } ItemsOptions.php 0000644 00000003562 15025064345 0007724 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use JsonMachine\Exception\InvalidArgumentException; use JsonMachine\JsonDecoder\ExtJsonDecoder; use JsonMachine\JsonDecoder\ItemDecoder; class ItemsOptions extends \ArrayObject { private $options = []; public function __construct(array $options = []) { $this->validateOptions($options); parent::__construct($this->options); } public function toArray(): array { return $this->options; } /** * @throws InvalidArgumentException */ private function validateOptions(array $options) { $mergedOptions = array_merge(self::defaultOptions(), $options); try { foreach ($mergedOptions as $optionName => $optionValue) { if ( ! isset(self::defaultOptions()[$optionName])) { throw new InvalidArgumentException("Option '$optionName' does not exist."); } $this->options[$optionName] = $this->{"opt_$optionName"}($optionValue); } } catch (\TypeError $typeError) { throw new InvalidArgumentException( preg_replace('~Argument #[0-9]+~', "Option '$optionName'", $typeError->getMessage()) ); } } private function opt_pointer($pointer) { if (is_array($pointer)) { (function (string ...$p) {})(...$pointer); } else { (function (string $p) {})($pointer); } return $pointer; } private function opt_decoder(ItemDecoder $decoder = null) { return $decoder; } private function opt_debug(bool $debug) { return $debug; } public static function defaultOptions(): array { return [ 'pointer' => '', 'decoder' => new ExtJsonDecoder(), 'debug' => false, ]; } } FileChunks.php 0000644 00000001357 15025064345 0007322 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; class FileChunks implements \IteratorAggregate { /** @var string */ private $fileName; /** @var int */ private $chunkSize; /** * @param string $fileName * @param int $chunkSize */ public function __construct($fileName, $chunkSize = 1024 * 8) { $this->fileName = $fileName; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { $fileHandle = fopen($this->fileName, 'r'); try { yield from new StreamChunks($fileHandle, $this->chunkSize); } finally { fclose($fileHandle); } } } PositionAware.php 0000644 00000000341 15025064345 0010043 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; interface PositionAware { /** * Returns a number of processed bytes from the beginning. * * @return int */ public function getPosition(); } StreamChunks.php 0000644 00000001612 15025064345 0007670 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use JsonMachine\Exception\InvalidArgumentException; class StreamChunks implements \IteratorAggregate { /** @var resource */ private $stream; /** @var int */ private $chunkSize; /** * @param resource $stream * @param int $chunkSize */ public function __construct($stream, $chunkSize = 1024 * 8) { if ( ! is_resource($stream) || get_resource_type($stream) !== 'stream') { throw new InvalidArgumentException('Argument $stream must be a valid stream resource.'); } $this->stream = $stream; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { while ('' !== ($chunk = fread($this->stream, $this->chunkSize))) { yield $chunk; } } } Exception/JsonMachineException.php 0000644 00000000165 15025064345 0013276 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\Exception; class JsonMachineException extends \Exception { } Exception/PathNotFoundException.php 0000644 00000000200 15025064345 0013437 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\Exception; class PathNotFoundException extends JsonMachineException { } Exception/SyntaxErrorException.php 0000644 00000000401 15025064345 0013371 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\Exception; class SyntaxErrorException extends JsonMachineException { public function __construct($message, $position) { parent::__construct($message." At position $position."); } } Exception/UnexpectedEndSyntaxErrorException.php 0000644 00000000214 15025064345 0016047 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\Exception; class UnexpectedEndSyntaxErrorException extends SyntaxErrorException { } JsonDecoder/DecodingError.php 0000644 00000001217 15025064345 0012207 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class DecodingError { private $malformedJson; private $errorMessage; /** * @param string $malformedJson * @param string $errorMessage */ public function __construct($malformedJson, $errorMessage) { $this->malformedJson = $malformedJson; $this->errorMessage = $errorMessage; } /** * @return string */ public function getMalformedJson() { return $this->malformedJson; } /** * @return string */ public function getErrorMessage() { return $this->errorMessage; } } JsonDecoder/ItemDecoder.php 0000644 00000000436 15025064345 0011647 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; interface ItemDecoder { /** * Decodes composite or scalar JSON values which are directly yielded to the user. * * @return InvalidResult|ValidResult */ public function decode($jsonValue); } JsonDecoder/ExtJsonDecoder.php 0000644 00000001403 15025064345 0012336 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class ExtJsonDecoder implements ItemDecoder { /** * @var bool */ private $assoc; /** * @var int */ private $depth; /** * @var int */ private $options; public function __construct($assoc = false, $depth = 512, $options = 0) { $this->assoc = $assoc; $this->depth = $depth; $this->options = $options; } public function decode($jsonValue) { $decoded = json_decode($jsonValue, $this->assoc, $this->depth, $this->options); if (json_last_error() !== JSON_ERROR_NONE) { return new InvalidResult(json_last_error_msg()); } return new ValidResult($decoded); } } JsonDecoder/InvalidResult.php 0000644 00000000656 15025064345 0012254 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class InvalidResult { /** * @var string */ private $errorMessage; public function __construct(string $errorMessage) { $this->errorMessage = $errorMessage; } public function getErrorMessage(): string { return $this->errorMessage; } public function isOk(): bool { return false; } } JsonDecoder/PassThruDecoder.php 0000644 00000000325 15025064345 0012517 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class PassThruDecoder implements ItemDecoder { public function decode($jsonValue) { return new ValidResult($jsonValue); } } JsonDecoder/ErrorWrappingDecoder.php 0000644 00000001106 15025064345 0013545 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class ErrorWrappingDecoder implements ItemDecoder { /** * @var ItemDecoder */ private $innerDecoder; public function __construct(ItemDecoder $innerDecoder) { $this->innerDecoder = $innerDecoder; } public function decode($jsonValue) { $result = $this->innerDecoder->decode($jsonValue); if ( ! $result->isOk()) { return new ValidResult(new DecodingError($jsonValue, $result->getErrorMessage())); } return $result; } } JsonDecoder/ValidResult.php 0000644 00000000626 15025064345 0011722 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine\JsonDecoder; class ValidResult { /** * @var mixed */ private $value; public function __construct($value) { $this->value = $value; } /** * @return mixed */ public function getValue() { return $this->value; } public function isOk(): bool { return true; } } Tokens.php 0000644 00000007114 15025064345 0006527 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use Generator; class Tokens implements \IteratorAggregate, PositionAware { /** @var iterable */ private $jsonChunks; /** * @param iterable<string> $jsonChunks */ public function __construct($jsonChunks) { $this->jsonChunks = $jsonChunks; } /** * @return Generator */ #[\ReturnTypeWillChange] public function getIterator() { $insignificantBytes = $this->insignificantBytes(); $tokenBoundaries = $this->tokenBoundaries(); $colonCommaBracket = $this->colonCommaBracketTokenBoundaries(); $inString = false; $tokenBuffer = ''; $escaping = false; foreach ($this->jsonChunks as $jsonChunk) { $bytesLength = strlen($jsonChunk); for ($i = 0; $i < $bytesLength; ++$i) { $byte = $jsonChunk[$i]; if ($escaping) { $escaping = false; $tokenBuffer .= $byte; continue; } if (isset($insignificantBytes[$byte])) { // is a JSON-structure insignificant byte $tokenBuffer .= $byte; continue; } if ($inString) { if ($byte == '"') { $inString = false; } elseif ($byte == '\\') { $escaping = true; } $tokenBuffer .= $byte; continue; } if (isset($tokenBoundaries[$byte])) { if ($tokenBuffer != '') { yield $tokenBuffer; $tokenBuffer = ''; } if (isset($colonCommaBracket[$byte])) { yield $byte; } } else { // else branch matches `"` but also `\` outside of a string literal which is an error anyway but strictly speaking not correctly parsed token $inString = true; $tokenBuffer .= $byte; } } } if ($tokenBuffer != '') { yield $tokenBuffer; } } private function tokenBoundaries() { $utf8bom1 = "\xEF"; $utf8bom2 = "\xBB"; $utf8bom3 = "\xBF"; return array_merge( [ $utf8bom1 => true, $utf8bom2 => true, $utf8bom3 => true, ' ' => true, "\n" => true, "\r" => true, "\t" => true, ], $this->colonCommaBracketTokenBoundaries() ); } private function colonCommaBracketTokenBoundaries(): array { return [ '{' => true, '}' => true, '[' => true, ']' => true, ':' => true, ',' => true, ]; } private function insignificantBytes(): array { $insignificantBytes = []; foreach (range(0, 255) as $ord) { if ( ! in_array( chr($ord), ['\\', '"', "\xEF", "\xBB", "\xBF", ' ', "\n", "\r", "\t", '{', '}', '[', ']', ':', ','] )) { $insignificantBytes[chr($ord)] = true; } } return $insignificantBytes; } public function getPosition(): int { return 0; } public function getLine(): int { return 1; } public function getColumn(): int { return 0; } } Parser.php 0000644 00000034444 15025064345 0006526 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use JsonMachine\Exception\InvalidArgumentException; use JsonMachine\Exception\JsonMachineException; use JsonMachine\Exception\PathNotFoundException; use JsonMachine\Exception\SyntaxErrorException; use JsonMachine\Exception\UnexpectedEndSyntaxErrorException; use JsonMachine\JsonDecoder\ExtJsonDecoder; use JsonMachine\JsonDecoder\ItemDecoder; use Traversable; class Parser implements \IteratorAggregate, PositionAware { const SCALAR_CONST = 1; const SCALAR_STRING = 2; const OBJECT_START = 4; const OBJECT_END = 8; const ARRAY_START = 16; const ARRAY_END = 32; const COMMA = 64; const COLON = 128; const SCALAR_VALUE = self::SCALAR_CONST | self::SCALAR_STRING; const ANY_VALUE = self::OBJECT_START | self::ARRAY_START | self::SCALAR_CONST | self::SCALAR_STRING; const AFTER_ARRAY_START = self::ANY_VALUE | self::ARRAY_END; const AFTER_OBJECT_START = self::SCALAR_STRING | self::OBJECT_END; const AFTER_ARRAY_VALUE = self::COMMA | self::ARRAY_END; const AFTER_OBJECT_VALUE = self::COMMA | self::OBJECT_END; /** @var Traversable */ private $tokens; /** @var ItemDecoder */ private $jsonDecoder; /** @var string */ private $matchedJsonPointer; /** @var array */ private $paths; /** @var array */ private $currentPath; /** @var array */ private $jsonPointers; /** @var bool */ private $hasSingleJsonPointer; /** * @param array|string $jsonPointer Follows json pointer RFC https://tools.ietf.org/html/rfc6901 * @param ItemDecoder $jsonDecoder * * @throws InvalidArgumentException */ public function __construct(Traversable $tokens, $jsonPointer = '', ItemDecoder $jsonDecoder = null) { $jsonPointers = (new ValidJsonPointers((array) $jsonPointer))->toArray(); $this->tokens = $tokens; $this->jsonDecoder = $jsonDecoder ?: new ExtJsonDecoder(); $this->hasSingleJsonPointer = (count($jsonPointers) === 1); $this->jsonPointers = array_combine($jsonPointers, $jsonPointers); $this->paths = $this->buildPaths($this->jsonPointers); } private function buildPaths(array $jsonPointers): array { return array_map(function ($jsonPointer) { return self::jsonPointerToPath($jsonPointer); }, $jsonPointers); } /** * @return \Generator * * @throws PathNotFoundException */ #[\ReturnTypeWillChange] public function getIterator() { $tokenTypes = $this->tokenTypes(); $iteratorStruct = null; $currentPath = &$this->currentPath; $currentPath = []; $currentPathWildcard = []; $pointersFound = []; $currentLevel = -1; $stack = [$currentLevel => null]; $jsonBuffer = ''; $key = null; $objectKeyExpected = false; $inObject = true; // hack to make "!$inObject" in first iteration work. Better code structure? $expectedType = self::OBJECT_START | self::ARRAY_START; $subtreeEnded = false; $token = null; $currentPathChanged = true; $jsonPointerPath = []; $iteratorLevel = 0; // local variables for faster name lookups $tokens = $this->tokens; foreach ($tokens as $token) { if ($currentPathChanged) { $currentPathChanged = false; $jsonPointerPath = $this->getMatchingJsonPointerPath(); $iteratorLevel = count($jsonPointerPath); } $tokenType = $tokenTypes[$token[0]]; if (0 == ($tokenType & $expectedType)) { $this->error('Unexpected symbol', $token); } $isValue = ($tokenType | 23) == 23; // 23 = self::ANY_VALUE if ( ! $inObject && $isValue && $currentLevel < $iteratorLevel) { $currentPathChanged = ! $this->hasSingleJsonPointer; $currentPath[$currentLevel] = isset($currentPath[$currentLevel]) ? (string) (1 + (int) $currentPath[$currentLevel]) : '0'; $currentPathWildcard[$currentLevel] = preg_match('/^(?:\d+|-)$/S', $jsonPointerPath[$currentLevel]) ? '-' : $currentPath[$currentLevel]; unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1], $stack[$currentLevel + 1]); } if ( ( $jsonPointerPath == $currentPath || $jsonPointerPath == $currentPathWildcard ) && ( $currentLevel > $iteratorLevel || ( ! $objectKeyExpected && ( ($currentLevel == $iteratorLevel && $isValue) || ($currentLevel + 1 == $iteratorLevel && ($tokenType | 3) == 3) // 3 = self::SCALAR_VALUE ) ) ) ) { $jsonBuffer .= $token; } // todo move this switch to the top just after the syntax check to be a correct FSM switch ($token[0]) { case '"': if ($objectKeyExpected) { $objectKeyExpected = false; $expectedType = 128; // 128 = self::COLON if ($currentLevel == $iteratorLevel) { $key = $token; } elseif ($currentLevel < $iteratorLevel) { $key = $token; $referenceToken = substr($token, 1, -1); $currentPathChanged = ! $this->hasSingleJsonPointer; $currentPath[$currentLevel] = $referenceToken; $currentPathWildcard[$currentLevel] = $referenceToken; unset($currentPath[$currentLevel + 1], $currentPathWildcard[$currentLevel + 1]); } continue 2; // a valid json chunk is not completed yet } if ($inObject) { $expectedType = 72; // 72 = self::AFTER_OBJECT_VALUE; } else { $expectedType = 96; // 96 = self::AFTER_ARRAY_VALUE; } break; case ',': if ($inObject) { $objectKeyExpected = true; $expectedType = 2; // 2 = self::SCALAR_STRING } else { $expectedType = 23; // 23 = self::ANY_VALUE } continue 2; // a valid json chunk is not completed yet case ':': $expectedType = 23; // 23 = self::ANY_VALUE continue 2; // a valid json chunk is not completed yet case '{': ++$currentLevel; if ($currentLevel <= $iteratorLevel) { $iteratorStruct = '{'; } $stack[$currentLevel] = '{'; $inObject = true; $expectedType = 10; // 10 = self::AFTER_OBJECT_START $objectKeyExpected = true; continue 2; // a valid json chunk is not completed yet case '[': ++$currentLevel; if ($currentLevel <= $iteratorLevel) { $iteratorStruct = '['; } $stack[$currentLevel] = '['; $inObject = false; $expectedType = 55; // 55 = self::AFTER_ARRAY_START; continue 2; // a valid json chunk is not completed yet case '}': $objectKeyExpected = false; // no break case ']': --$currentLevel; $inObject = $stack[$currentLevel] == '{'; // no break default: if ($inObject) { $expectedType = 72; // 72 = self::AFTER_OBJECT_VALUE; } else { $expectedType = 96; // 96 = self::AFTER_ARRAY_VALUE; } } if ($currentLevel > $iteratorLevel) { continue; // a valid json chunk is not completed yet } if ($jsonBuffer !== '') { $valueResult = $this->jsonDecoder->decode($jsonBuffer); $jsonBuffer = ''; if ( ! $valueResult->isOk()) { $this->error($valueResult->getErrorMessage(), $token); } if ($iteratorStruct == '[') { yield $valueResult->getValue(); } else { $keyResult = $this->jsonDecoder->decode($key); if ( ! $keyResult->isOk()) { $this->error($keyResult->getErrorMessage(), $key); } yield $keyResult->getValue() => $valueResult->getValue(); unset($keyResult); } unset($valueResult); } if ($jsonPointerPath == $currentPath || $jsonPointerPath == $currentPathWildcard) { if ( ! in_array($this->matchedJsonPointer, $pointersFound, true)) { $pointersFound[] = $this->matchedJsonPointer; } } elseif (count($pointersFound) == count($this->jsonPointers)) { $subtreeEnded = true; break; } } if ($token === null) { $this->error('Cannot iterate empty JSON', $token); } if ($currentLevel > -1 && ! $subtreeEnded) { $this->error('JSON string ended unexpectedly', $token, UnexpectedEndSyntaxErrorException::class); } if (count($pointersFound) !== count($this->jsonPointers)) { throw new PathNotFoundException(sprintf("Paths '%s' were not found in json stream.", implode(', ', array_diff($this->jsonPointers, $pointersFound)))); } $this->matchedJsonPointer = null; $this->currentPath = null; } private function tokenTypes() { return [ 'n' => self::SCALAR_CONST, 't' => self::SCALAR_CONST, 'f' => self::SCALAR_CONST, '-' => self::SCALAR_CONST, '0' => self::SCALAR_CONST, '1' => self::SCALAR_CONST, '2' => self::SCALAR_CONST, '3' => self::SCALAR_CONST, '4' => self::SCALAR_CONST, '5' => self::SCALAR_CONST, '6' => self::SCALAR_CONST, '7' => self::SCALAR_CONST, '8' => self::SCALAR_CONST, '9' => self::SCALAR_CONST, '"' => self::SCALAR_STRING, '{' => self::OBJECT_START, '}' => self::OBJECT_END, '[' => self::ARRAY_START, ']' => self::ARRAY_END, ',' => self::COMMA, ':' => self::COLON, ]; } private function getMatchingJsonPointerPath(): array { $matchingPointer = key($this->paths); if (count($this->paths) === 1) { $this->matchedJsonPointer = $matchingPointer; return $this->paths[$matchingPointer]; } $currentPathLength = count($this->currentPath); $matchLength = -1; foreach ($this->paths as $jsonPointer => $path) { $matchingReferenceTokens = []; foreach ($path as $i => $referenceToken) { if ( ! isset($this->currentPath[$i]) || ( $this->currentPath[$i] !== $referenceToken && ValidJsonPointers::wildcardify($this->currentPath[$i]) !== $referenceToken ) ) { continue; } $matchingReferenceTokens[$i] = $referenceToken; } if (empty($matchingReferenceTokens)) { continue; } $currentMatchLength = count($matchingReferenceTokens); if ($currentMatchLength > $matchLength) { $matchingPointer = $jsonPointer; $matchLength = $currentMatchLength; } if ($matchLength === $currentPathLength) { break; } } $this->matchedJsonPointer = $matchingPointer; return $this->paths[$matchingPointer]; } public function getJsonPointers(): array { return array_values($this->jsonPointers); } public function getCurrentJsonPointer(): string { if ($this->currentPath === null) { throw new JsonMachineException(__METHOD__.' must be called inside a loop'); } return self::pathToJsonPointer($this->currentPath); } public function getMatchedJsonPointer(): string { if ($this->matchedJsonPointer === null) { throw new JsonMachineException(__METHOD__.' must be called inside a loop'); } return $this->matchedJsonPointer; } /** * @param string $msg * @param string $token * @param string $exception */ private function error($msg, $token, $exception = SyntaxErrorException::class) { throw new $exception($msg." '".$token."'", $this->tokens->getPosition()); } /** * @return int * * @throws JsonMachineException */ public function getPosition() { if ($this->tokens instanceof PositionAware) { return $this->tokens->getPosition(); } throw new JsonMachineException('Provided tokens iterable must implement PositionAware to call getPosition on it.'); } private static function jsonPointerToPath(string $jsonPointer): array { return array_slice(array_map(function ($jsonPointerPart) { return str_replace(['~1', '~0'], ['/', '~'], $jsonPointerPart); }, explode('/', $jsonPointer)), 1); } private static function pathToJsonPointer(array $path): string { $encodedParts = array_map(function ($addressPart) { return str_replace(['~', '/'], ['~0', '~1'], $addressPart); }, $path); array_unshift($encodedParts, ''); return implode('/', $encodedParts); } } StringChunks.php 0000644 00000001310 15025064345 0007676 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; class StringChunks implements \IteratorAggregate { /** @var string */ private $string; /** @var int */ private $chunkSize; /** * @param string $string * @param int $chunkSize */ public function __construct($string, $chunkSize = 1024 * 8) { $this->string = $string; $this->chunkSize = $chunkSize; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { $len = strlen($this->string); for ($i = 0; $i < $len; $i += $this->chunkSize) { yield substr($this->string, $i, $this->chunkSize); } } } TokensWithDebugging.php 0000644 00000007377 15025064345 0011212 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; class TokensWithDebugging implements \IteratorAggregate, PositionAware { /** @var iterable */ private $jsonChunks; private $position = 0; private $line = 1; private $column = 0; /** * @param iterable<string> $jsonChunks */ public function __construct($jsonChunks) { $this->jsonChunks = $jsonChunks; } /** * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator() { // Treat UTF-8 BOM bytes as whitespace ${"\xEF"} = ${"\xBB"} = ${"\xBF"} = 0; ${' '} = 0; ${"\n"} = 0; ${"\r"} = 0; ${"\t"} = 0; ${'{'} = 1; ${'}'} = 1; ${'['} = 1; ${']'} = 1; ${':'} = 1; ${','} = 1; $inString = false; $tokenBuffer = ''; $escaping = false; $tokenWidth = 0; $ignoreLF = false; $position = 0; $line = 1; $column = 0; foreach ($this->jsonChunks as $bytes) { $bytesLength = strlen($bytes); for ($i = 0; $i < $bytesLength; ++$i) { $byte = $bytes[$i]; if ($inString) { if ($byte == '"' && ! $escaping) { $inString = false; } $escaping = ($byte == '\\' && ! $escaping); $tokenBuffer .= $byte; ++$tokenWidth; continue; } if (isset($$byte)) { ++$column; if ($tokenBuffer != '') { $this->position = $position + $i; $this->column = $column; $this->line = $line; yield $tokenBuffer; $column += $tokenWidth; $tokenBuffer = ''; $tokenWidth = 0; } if ($$byte) { // is not whitespace $this->position = $position + $i + 1; $this->column = $column; $this->line = $line; yield $byte; // track line number and reset column for each newline } elseif ($byte == "\n") { // handle CRLF newlines if ($ignoreLF) { --$column; $ignoreLF = false; continue; } ++$line; $column = 0; } elseif ($byte == "\r") { ++$line; $ignoreLF = true; $column = 0; } } else { if ($byte == '"') { $inString = true; } $tokenBuffer .= $byte; ++$tokenWidth; } } $position += $i; } $this->position = $position; if ($tokenBuffer != '') { $this->column = $column; yield $tokenBuffer; } } /** * @return int */ public function getPosition() { return $this->position; } /** * Returns the line number of the lexeme currently being processed (index starts at one). * * @return int */ public function getLine() { return $this->line; } /** * The position of currently being processed lexeme within the line (index starts at one). * * @return int */ public function getColumn() { return $this->column; } } ValidJsonPointers.php 0000644 00000005034 15025064345 0010700 0 ustar 00 <?php declare(strict_types=1); namespace JsonMachine; use JsonMachine\Exception\InvalidArgumentException; final class ValidJsonPointers { private $jsonPointers = []; private $validated = false; public function __construct(array $jsonPointers) { $this->jsonPointers = array_values($jsonPointers); } /** * @throws InvalidArgumentException */ public function toArray(): array { if ( ! $this->validated) { $this->validate(); } return $this->jsonPointers; } /** * @throws InvalidArgumentException */ private function validate() { $this->validateFormat(); $this->validateJsonPointersDoNotIntersect(); $this->validated = true; } /** * @throws InvalidArgumentException */ private function validateFormat() { foreach ($this->jsonPointers as $jsonPointerEl) { if (preg_match('_^(/(([^/~])|(~[01]))*)*$_', $jsonPointerEl) === 0) { throw new InvalidArgumentException( sprintf("Given value '%s' of \$jsonPointer is not valid JSON Pointer", $jsonPointerEl) ); } } } /** * @throws InvalidArgumentException */ private function validateJsonPointersDoNotIntersect() { foreach ($this->jsonPointers as $keyA => $jsonPointerA) { foreach ($this->jsonPointers as $keyB => $jsonPointerB) { if ($keyA === $keyB) { continue; } if ($jsonPointerA === $jsonPointerB || self::str_contains($jsonPointerA, $jsonPointerB) || self::str_contains($jsonPointerA, self::wildcardify($jsonPointerB)) ) { throw new InvalidArgumentException( sprintf( "JSON Pointers must not intersect. At least these two do: '%s', '%s'", $jsonPointerA, $jsonPointerB ) ); } } } } public static function wildcardify(string $jsonPointerPart): string { return preg_replace('~/\d+(/|$)~S', '/-$1', $jsonPointerPart); } /** * @see https://github.com/symfony/polyfill/blob/v1.24.0/src/Php80/Php80.php */ public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } } NullValue.php 0000644 00000002131 15025064625 0007166 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum; use DASPRiD\Enum\Exception\CloneNotSupportedException; use DASPRiD\Enum\Exception\SerializeNotSupportedException; use DASPRiD\Enum\Exception\UnserializeNotSupportedException; final class NullValue { /** * @var self */ private static $instance; private function __construct() { } public static function instance() : self { return self::$instance ?: self::$instance = new self(); } /** * Forbid cloning enums. * * @throws CloneNotSupportedException */ final public function __clone() { throw new CloneNotSupportedException(); } /** * Forbid serializing enums. * * @throws SerializeNotSupportedException */ final public function __sleep() : array { throw new SerializeNotSupportedException(); } /** * Forbid unserializing enums. * * @throws UnserializeNotSupportedException */ final public function __wakeup() : void { throw new UnserializeNotSupportedException(); } } EnumMap.php 0000644 00000025724 15025064625 0006636 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum; use DASPRiD\Enum\Exception\ExpectationException; use DASPRiD\Enum\Exception\IllegalArgumentException; use IteratorAggregate; use Serializable; use Traversable; /** * A specialized map implementation for use with enum type keys. * * All of the keys in an enum map must come from a single enum type that is specified, when the map is created. Enum * maps are represented internally as arrays. This representation is extremely compact and efficient. * * Enum maps are maintained in the natural order of their keys (the order in which the enum constants are declared). * This is reflected in the iterators returned by the collection views {@see self::getIterator()} and * {@see self::values()}. * * Iterators returned by the collection views are not consistent: They may or may not show the effects of modifications * to the map that occur while the iteration is in progress. */ final class EnumMap implements Serializable, IteratorAggregate { /** * The class name of the key. * * @var string */ private $keyType; /** * The type of the value. * * @var string */ private $valueType; /** * @var bool */ private $allowNullValues; /** * All of the constants comprising the enum, cached for performance. * * @var array<int, AbstractEnum> */ private $keyUniverse; /** * Array representation of this map. The ith element is the value to which universe[i] is currently mapped, or null * if it isn't mapped to anything, or NullValue if it's mapped to null. * * @var array<int, mixed> */ private $values; /** * @var int */ private $size = 0; /** * Creates a new enum map. * * @param string $keyType the type of the keys, must extend AbstractEnum * @param string $valueType the type of the values * @param bool $allowNullValues whether to allow null values * @throws IllegalArgumentException when key type does not extend AbstractEnum */ public function __construct(string $keyType, string $valueType, bool $allowNullValues) { if (! is_subclass_of($keyType, AbstractEnum::class)) { throw new IllegalArgumentException(sprintf( 'Class %s does not extend %s', $keyType, AbstractEnum::class )); } $this->keyType = $keyType; $this->valueType = $valueType; $this->allowNullValues = $allowNullValues; $this->keyUniverse = $keyType::values(); $this->values = array_fill(0, count($this->keyUniverse), null); } /** * Checks whether the map types match the supplied ones. * * You should call this method when an EnumMap is passed to you and you want to ensure that it's made up of the * correct types. * * @throws ExpectationException when supplied key type mismatches local key type * @throws ExpectationException when supplied value type mismatches local value type * @throws ExpectationException when the supplied map allows null values, abut should not */ public function expect(string $keyType, string $valueType, bool $allowNullValues) : void { if ($keyType !== $this->keyType) { throw new ExpectationException(sprintf( 'Callee expected an EnumMap with key type %s, but got %s', $keyType, $this->keyType )); } if ($valueType !== $this->valueType) { throw new ExpectationException(sprintf( 'Callee expected an EnumMap with value type %s, but got %s', $keyType, $this->keyType )); } if ($allowNullValues !== $this->allowNullValues) { throw new ExpectationException(sprintf( 'Callee expected an EnumMap with nullable flag %s, but got %s', ($allowNullValues ? 'true' : 'false'), ($this->allowNullValues ? 'true' : 'false') )); } } /** * Returns the number of key-value mappings in this map. */ public function size() : int { return $this->size; } /** * Returns true if this map maps one or more keys to the specified value. */ public function containsValue($value) : bool { return in_array($this->maskNull($value), $this->values, true); } /** * Returns true if this map contains a mapping for the specified key. */ public function containsKey(AbstractEnum $key) : bool { $this->checkKeyType($key); return null !== $this->values[$key->ordinal()]; } /** * Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. * * More formally, if this map contains a mapping from a key to a value, then this method returns the value; * otherwise it returns null (there can be at most one such mapping). * * A return value of null does not necessarily indicate that the map contains no mapping for the key; it's also * possible that hte map explicitly maps the key to null. The {@see self::containsKey()} operation may be used to * distinguish these two cases. * * @return mixed */ public function get(AbstractEnum $key) { $this->checkKeyType($key); return $this->unmaskNull($this->values[$key->ordinal()]); } /** * Associates the specified value with the specified key in this map. * * If the map previously contained a mapping for this key, the old value is replaced. * * @return mixed the previous value associated with the specified key, or null if there was no mapping for the key. * (a null return can also indicate that the map previously associated null with the specified key.) * @throws IllegalArgumentException when the passed values does not match the internal value type */ public function put(AbstractEnum $key, $value) { $this->checkKeyType($key); if (! $this->isValidValue($value)) { throw new IllegalArgumentException(sprintf('Value is not of type %s', $this->valueType)); } $index = $key->ordinal(); $oldValue = $this->values[$index]; $this->values[$index] = $this->maskNull($value); if (null === $oldValue) { ++$this->size; } return $this->unmaskNull($oldValue); } /** * Removes the mapping for this key frm this map if present. * * @return mixed the previous value associated with the specified key, or null if there was no mapping for the key. * (a null return can also indicate that the map previously associated null with the specified key.) */ public function remove(AbstractEnum $key) { $this->checkKeyType($key); $index = $key->ordinal(); $oldValue = $this->values[$index]; $this->values[$index] = null; if (null !== $oldValue) { --$this->size; } return $this->unmaskNull($oldValue); } /** * Removes all mappings from this map. */ public function clear() : void { $this->values = array_fill(0, count($this->keyUniverse), null); $this->size = 0; } /** * Compares the specified map with this map for quality. * * Returns true if the two maps represent the same mappings. */ public function equals(self $other) : bool { if ($this === $other) { return true; } if ($this->size !== $other->size) { return false; } return $this->values === $other->values; } /** * Returns the values contained in this map. * * The array will contain the values in the order their corresponding keys appear in the map, which is their natural * order (the order in which the num constants are declared). */ public function values() : array { return array_values(array_map(function ($value) { return $this->unmaskNull($value); }, array_filter($this->values, function ($value) : bool { return null !== $value; }))); } public function serialize() : string { $values = []; foreach ($this->values as $ordinal => $value) { if (null === $value) { continue; } $values[$ordinal] = $this->unmaskNull($value); } return serialize([ 'keyType' => $this->keyType, 'valueType' => $this->valueType, 'allowNullValues' => $this->allowNullValues, 'values' => $values, ]); } public function unserialize($serialized) : void { $data = unserialize($serialized); $this->__construct($data['keyType'], $data['valueType'], $data['allowNullValues']); foreach ($this->keyUniverse as $key) { if (array_key_exists($key->ordinal(), $data['values'])) { $this->put($key, $data['values'][$key->ordinal()]); } } } public function getIterator() : Traversable { foreach ($this->keyUniverse as $key) { if (null === $this->values[$key->ordinal()]) { continue; } yield $key => $this->unmaskNull($this->values[$key->ordinal()]); } } private function maskNull($value) { if (null === $value) { return NullValue::instance(); } return $value; } private function unmaskNull($value) { if ($value instanceof NullValue) { return null; } return $value; } /** * @throws IllegalArgumentException when the passed key does not match the internal key type */ private function checkKeyType(AbstractEnum $key) : void { if (get_class($key) !== $this->keyType) { throw new IllegalArgumentException(sprintf( 'Object of type %s is not the same type as %s', get_class($key), $this->keyType )); } } private function isValidValue($value) : bool { if (null === $value) { if ($this->allowNullValues) { return true; } return false; } switch ($this->valueType) { case 'mixed': return true; case 'bool': case 'boolean': return is_bool($value); case 'int': case 'integer': return is_int($value); case 'float': case 'double': return is_float($value); case 'string': return is_string($value); case 'object': return is_object($value); case 'array': return is_array($value); } return $value instanceof $this->valueType; } } Exception/IllegalArgumentException.php 0000644 00000000256 15025064625 0014156 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class IllegalArgumentException extends Exception implements ExceptionInterface { } Exception/UnserializeNotSupportedException.php 0000644 00000000266 15025064625 0015764 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class UnserializeNotSupportedException extends Exception implements ExceptionInterface { } Exception/SerializeNotSupportedException.php 0000644 00000000264 15025064625 0015417 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class SerializeNotSupportedException extends Exception implements ExceptionInterface { } Exception/ExpectationException.php 0000644 00000000252 15025064625 0013361 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class ExpectationException extends Exception implements ExceptionInterface { } Exception/MismatchException.php 0000644 00000000247 15025064625 0012647 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class MismatchException extends Exception implements ExceptionInterface { } Exception/CloneNotSupportedException.php 0000644 00000000260 15025064625 0014524 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum\Exception; use Exception; final class CloneNotSupportedException extends Exception implements ExceptionInterface { } AbstractEnum.php 0000644 00000015412 15025064625 0007655 0 ustar 00 <?php declare(strict_types = 1); namespace DASPRiD\Enum; use DASPRiD\Enum\Exception\CloneNotSupportedException; use DASPRiD\Enum\Exception\IllegalArgumentException; use DASPRiD\Enum\Exception\MismatchException; use DASPRiD\Enum\Exception\SerializeNotSupportedException; use DASPRiD\Enum\Exception\UnserializeNotSupportedException; use ReflectionClass; abstract class AbstractEnum { /** * @var string */ private $name; /** * @var int */ private $ordinal; /** * @var array<string, array<string, static>> */ private static $values = []; /** * @var array<string, bool> */ private static $allValuesLoaded = []; /** * @var array<string, array> */ private static $constants = []; /** * The constructor is private by default to avoid arbitrary enum creation. * * When creating your own constructor for a parameterized enum, make sure to declare it as protected, so that * the static methods are able to construct it. Avoid making it public, as that would allow creation of * non-singleton enum instances. */ private function __construct() { } /** * Magic getter which forwards all calls to {@see self::valueOf()}. * * @return static */ final public static function __callStatic(string $name, array $arguments) : self { return static::valueOf($name); } /** * Returns an enum with the specified name. * * The name must match exactly an identifier used to declare an enum in this type (extraneous whitespace characters * are not permitted). * * @return static * @throws IllegalArgumentException if the enum has no constant with the specified name */ final public static function valueOf(string $name) : self { if (isset(self::$values[static::class][$name])) { return self::$values[static::class][$name]; } $constants = self::constants(); if (array_key_exists($name, $constants)) { return self::createValue($name, $constants[$name][0], $constants[$name][1]); } throw new IllegalArgumentException(sprintf('No enum constant %s::%s', static::class, $name)); } /** * @return static */ private static function createValue(string $name, int $ordinal, array $arguments) : self { $instance = new static(...$arguments); $instance->name = $name; $instance->ordinal = $ordinal; self::$values[static::class][$name] = $instance; return $instance; } /** * Obtains all possible types defined by this enum. * * @return static[] */ final public static function values() : array { if (isset(self::$allValuesLoaded[static::class])) { return self::$values[static::class]; } if (! isset(self::$values[static::class])) { self::$values[static::class] = []; } foreach (self::constants() as $name => $constant) { if (array_key_exists($name, self::$values[static::class])) { continue; } static::createValue($name, $constant[0], $constant[1]); } uasort(self::$values[static::class], function (self $a, self $b) { return $a->ordinal() <=> $b->ordinal(); }); self::$allValuesLoaded[static::class] = true; return self::$values[static::class]; } private static function constants() : array { if (isset(self::$constants[static::class])) { return self::$constants[static::class]; } self::$constants[static::class] = []; $reflectionClass = new ReflectionClass(static::class); $ordinal = -1; foreach ($reflectionClass->getReflectionConstants() as $reflectionConstant) { if (! $reflectionConstant->isProtected()) { continue; } $value = $reflectionConstant->getValue(); self::$constants[static::class][$reflectionConstant->name] = [ ++$ordinal, is_array($value) ? $value : [] ]; } return self::$constants[static::class]; } /** * Returns the name of this enum constant, exactly as declared in its enum declaration. * * Most programmers should use the {@see self::__toString()} method in preference to this one, as the toString * method may return a more user-friendly name. This method is designed primarily for use in specialized situations * where correctness depends on getting the exact name, which will not vary from release to release. */ final public function name() : string { return $this->name; } /** * Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial * constant is assigned an ordinal of zero). * * Most programmers will have no use for this method. It is designed for use by sophisticated enum-based data * structures. */ final public function ordinal() : int { return $this->ordinal; } /** * Compares this enum with the specified object for order. * * Returns negative integer, zero or positive integer as this object is less than, equal to or greater than the * specified object. * * Enums are only comparable to other enums of the same type. The natural order implemented by this method is the * order in which the constants are declared. * * @throws MismatchException if the passed enum is not of the same type */ final public function compareTo(self $other) : int { if (! $other instanceof static) { throw new MismatchException(sprintf( 'The passed enum %s is not of the same type as %s', get_class($other), static::class )); } return $this->ordinal - $other->ordinal; } /** * Forbid cloning enums. * * @throws CloneNotSupportedException */ final public function __clone() { throw new CloneNotSupportedException(); } /** * Forbid serializing enums. * * @throws SerializeNotSupportedException */ final public function __sleep() : array { throw new SerializeNotSupportedException(); } /** * Forbid unserializing enums. * * @throws UnserializeNotSupportedException */ final public function __wakeup() : void { throw new UnserializeNotSupportedException(); } /** * Turns the enum into a string representation. * * You may override this method to give a more user-friendly version. */ public function __toString() : string { return $this->name; } } Pecee/SimpleRouter/Handlers/EventHandler.php 0000644 00000011452 15025064632 0015055 0 ustar 00 <?php namespace Pecee\SimpleRouter\Handlers; use Closure; use Pecee\SimpleRouter\Event\EventArgument; use Pecee\SimpleRouter\Router; class EventHandler implements IEventHandler { /** * Fires when a event is triggered. */ public const EVENT_ALL = '*'; /** * Fires when router is initializing and before routes are loaded. */ public const EVENT_INIT = 'onInit'; /** * Fires when all routes has been loaded and rendered, just before the output is returned. */ public const EVENT_LOAD = 'onLoad'; /** * Fires when route is added to the router */ public const EVENT_ADD_ROUTE = 'onAddRoute'; /** * Fires when a url-rewrite is and just before the routes are re-initialized. */ public const EVENT_REWRITE = 'onRewrite'; /** * Fires when the router is booting. * This happens just before boot-managers are rendered and before any routes has been loaded. */ public const EVENT_BOOT = 'onBoot'; /** * Fires before a boot-manager is rendered. */ public const EVENT_RENDER_BOOTMANAGER = 'onRenderBootManager'; /** * Fires when the router is about to load all routes. */ public const EVENT_LOAD_ROUTES = 'onLoadRoutes'; /** * Fires whenever the `findRoute` method is called within the `Router`. * This usually happens when the router tries to find routes that * contains a certain url, usually after the EventHandler::EVENT_GET_URL event. */ public const EVENT_FIND_ROUTE = 'onFindRoute'; /** * Fires whenever the `Router::getUrl` method or `url`-helper function * is called and the router tries to find the route. */ public const EVENT_GET_URL = 'onGetUrl'; /** * Fires when a route is matched and valid (correct request-type etc). * and before the route is rendered. */ public const EVENT_MATCH_ROUTE = 'onMatchRoute'; /** * Fires before a route is rendered. */ public const EVENT_RENDER_ROUTE = 'onRenderRoute'; /** * Fires when the router is loading exception-handlers. */ public const EVENT_LOAD_EXCEPTIONS = 'onLoadExceptions'; /** * Fires before the router is rendering a exception-handler. */ public const EVENT_RENDER_EXCEPTION = 'onRenderException'; /** * Fires before a middleware is rendered. */ public const EVENT_RENDER_MIDDLEWARES = 'onRenderMiddlewares'; /** * Fires before the CSRF-verifier is rendered. */ public const EVENT_RENDER_CSRF = 'onRenderCsrfVerifier'; /** * All available events * @var array */ public static $events = [ self::EVENT_ALL, self::EVENT_INIT, self::EVENT_LOAD, self::EVENT_ADD_ROUTE, self::EVENT_REWRITE, self::EVENT_BOOT, self::EVENT_RENDER_BOOTMANAGER, self::EVENT_LOAD_ROUTES, self::EVENT_FIND_ROUTE, self::EVENT_GET_URL, self::EVENT_MATCH_ROUTE, self::EVENT_RENDER_ROUTE, self::EVENT_LOAD_EXCEPTIONS, self::EVENT_RENDER_EXCEPTION, self::EVENT_RENDER_MIDDLEWARES, self::EVENT_RENDER_CSRF, ]; /** * List of all registered events * @var array */ private $registeredEvents = []; /** * Register new event * * @param string $name * @param Closure $callback * @return static */ public function register(string $name, Closure $callback): IEventHandler { if (isset($this->registeredEvents[$name]) === true) { $this->registeredEvents[$name][] = $callback; } else { $this->registeredEvents[$name] = [$callback]; } return $this; } /** * Get events. * * @param string|null $name Filter events by name. * @param array|string ...$names Add multiple names... * @return array */ public function getEvents(?string $name, ...$names): array { if ($name === null) { return $this->registeredEvents; } $names[] = $name; $events = []; foreach ($names as $eventName) { if (isset($this->registeredEvents[$eventName]) === true) { $events += $this->registeredEvents[$eventName]; } } return $events; } /** * Fires any events registered with given event-name * * @param Router $router Router instance * @param string $name Event name * @param array $eventArgs Event arguments */ public function fireEvents(Router $router, string $name, array $eventArgs = []): void { $events = $this->getEvents(static::EVENT_ALL, $name); /* @var $event Closure */ foreach ($events as $event) { $event(new EventArgument($name, $router, $eventArgs)); } } } Pecee/SimpleRouter/Handlers/IEventHandler.php 0000644 00000001105 15025064632 0015160 0 ustar 00 <?php namespace Pecee\SimpleRouter\Handlers; use Pecee\SimpleRouter\Router; interface IEventHandler { /** * Get events. * * @param string|null $name Filter events by name. * @return array */ public function getEvents(?string $name): array; /** * Fires any events registered with given event-name * * @param Router $router Router instance * @param string $name Event name * @param array $eventArgs Event arguments */ public function fireEvents(Router $router, string $name, array $eventArgs = []): void; } Pecee/SimpleRouter/Handlers/CallbackExceptionHandler.php 0000644 00000001562 15025064632 0017350 0 ustar 00 <?php namespace Pecee\SimpleRouter\Handlers; use Closure; use Exception; use Pecee\Http\Request; /** * Class CallbackExceptionHandler * * Class is used to create callbacks which are fired when an exception is reached. * This allows for easy handling 404-exception etc. without creating an custom ExceptionHandler. * * @package \Pecee\SimpleRouter\Handlers */ class CallbackExceptionHandler implements IExceptionHandler { /** * @var Closure */ protected $callback; public function __construct(Closure $callback) { $this->callback = $callback; } /** * @param Request $request * @param Exception $error */ public function handleError(Request $request, Exception $error): void { /* Fire exceptions */ call_user_func($this->callback, $request, $error ); } } Pecee/SimpleRouter/Handlers/IExceptionHandler.php 0000644 00000000420 15025064632 0016034 0 ustar 00 <?php namespace Pecee\SimpleRouter\Handlers; use Exception; use Pecee\Http\Request; interface IExceptionHandler { /** * @param Request $request * @param Exception $error */ public function handleError(Request $request, Exception $error): void; } Pecee/SimpleRouter/Handlers/DebugEventHandler.php 0000644 00000002451 15025064632 0016023 0 ustar 00 <?php namespace Pecee\SimpleRouter\Handlers; use Closure; use Pecee\SimpleRouter\Event\EventArgument; use Pecee\SimpleRouter\Router; class DebugEventHandler implements IEventHandler { /** * Debug callback * @var Closure */ protected $callback; public function __construct() { $this->callback = static function (EventArgument $argument): void { // todo: log in database }; } /** * Get events. * * @param string|null $name Filter events by name. * @return array */ public function getEvents(?string $name): array { return [ $name => [ $this->callback, ], ]; } /** * Fires any events registered with given event-name * * @param Router $router Router instance * @param string $name Event name * @param array $eventArgs Event arguments */ public function fireEvents(Router $router, string $name, array $eventArgs = []): void { $callback = $this->callback; $callback(new EventArgument($name, $router, $eventArgs)); } /** * Set debug callback * * @param Closure $event */ public function setCallback(Closure $event): void { $this->callback = $event; } } Pecee/SimpleRouter/SimpleRouter.php 0000644 00000036656 15025064632 0013405 0 ustar 00 <?php /** * --------------------------- * Router helper class * --------------------------- * * This class is added so calls can be made statically like SimpleRouter::get() making the code look pretty. * It also adds some extra functionality like default-namespace etc. */ namespace Pecee\SimpleRouter; use Closure; use Exception; use Pecee\Exceptions\InvalidArgumentException; use Pecee\Http\Middleware\BaseCsrfVerifier; use Pecee\Http\Request; use Pecee\Http\Response; use Pecee\Http\Url; use Pecee\SimpleRouter\ClassLoader\IClassLoader; use Pecee\SimpleRouter\Exceptions\HttpException; use Pecee\SimpleRouter\Handlers\CallbackExceptionHandler; use Pecee\SimpleRouter\Handlers\IEventHandler; use Pecee\SimpleRouter\Route\IGroupRoute; use Pecee\SimpleRouter\Route\ILoadableRoute; use Pecee\SimpleRouter\Route\IPartialGroupRoute; use Pecee\SimpleRouter\Route\IRoute; use Pecee\SimpleRouter\Route\RouteController; use Pecee\SimpleRouter\Route\RouteGroup; use Pecee\SimpleRouter\Route\RoutePartialGroup; use Pecee\SimpleRouter\Route\RouteResource; use Pecee\SimpleRouter\Route\RouteUrl; class SimpleRouter { /** * Default namespace added to all routes * @var string|null */ protected static $defaultNamespace; /** * The response object * @var Response */ protected static $response; /** * Router instance * @var Router */ protected static $router; /** * Start routing * * @throws \Pecee\SimpleRouter\Exceptions\NotFoundHttpException * @throws \Pecee\Http\Middleware\Exceptions\TokenMismatchException * @throws HttpException * @throws Exception */ public static function start(): void { // Set default namespaces foreach (static::router()->getRoutes() as $route) { static::addDefaultNamespace($route); } echo static::router()->start(); } /** * Start the routing an return array with debugging-information * * @return array */ public static function startDebug(): array { $routerOutput = null; try { ob_start(); static::router()->setDebugEnabled(true)->start(); $routerOutput = ob_get_clean(); } catch (Exception $e) { } // Try to parse library version $composerFile = dirname(__DIR__, 3) . '/composer.lock'; $version = false; if (is_file($composerFile) === true) { $composerInfo = json_decode(file_get_contents($composerFile), true); if (isset($composerInfo['packages']) === true && is_array($composerInfo['packages']) === true) { foreach ($composerInfo['packages'] as $package) { if (isset($package['name']) === true && strtolower($package['name']) === 'pecee/simple-router') { $version = $package['version']; break; } } } } $request = static::request(); $router = static::router(); return [ 'url' => $request->getUrl(), 'method' => $request->getMethod(), 'host' => $request->getHost(), 'loaded_routes' => $request->getLoadedRoutes(), 'all_routes' => $router->getRoutes(), 'boot_managers' => $router->getBootManagers(), 'csrf_verifier' => $router->getCsrfVerifier(), 'log' => $router->getDebugLog(), 'event_handlers' => $router->getEventHandlers(), 'router_output' => $routerOutput, 'library_version' => $version, 'php_version' => PHP_VERSION, 'server_params' => $request->getHeaders(), ]; } /** * Set default namespace which will be prepended to all routes. * * @param string $defaultNamespace */ public static function setDefaultNamespace(string $defaultNamespace): void { static::$defaultNamespace = $defaultNamespace; } /** * Base CSRF verifier * * @param BaseCsrfVerifier $baseCsrfVerifier */ public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier): void { static::router()->setCsrfVerifier($baseCsrfVerifier); } /** * Add new event handler to the router * * @param IEventHandler $eventHandler */ public static function addEventHandler(IEventHandler $eventHandler): void { static::router()->addEventHandler($eventHandler); } /** * Boot managers allows you to alter the routes before the routing occurs. * Perfect if you want to load pretty-urls from a file or database. * * @param IRouterBootManager $bootManager */ public static function addBootManager(IRouterBootManager $bootManager): void { static::router()->addBootManager($bootManager); } /** * Redirect to when route matches. * * @param string $where * @param string $to * @param int $httpCode * @return IRoute */ public static function redirect(string $where, string $to, int $httpCode = 301): IRoute { return static::get($where, static function () use ($to, $httpCode): void { static::response()->redirect($to, $httpCode); }); } /** * Route the given url to your callback on GET request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * * @return RouteUrl|IRoute */ public static function get(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_GET], $url, $callback, $settings); } /** * Route the given url to your callback on POST request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function post(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_POST], $url, $callback, $settings); } /** * Route the given url to your callback on PUT request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function put(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_PUT], $url, $callback, $settings); } /** * Route the given url to your callback on PATCH request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function patch(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_PATCH], $url, $callback, $settings); } /** * Route the given url to your callback on OPTIONS request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function options(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_OPTIONS], $url, $callback, $settings); } /** * Route the given url to your callback on DELETE request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function delete(string $url, $callback, array $settings = null): IRoute { return static::match([Request::REQUEST_TYPE_DELETE], $url, $callback, $settings); } /** * Groups allows for encapsulating routes with special settings. * * @param array $settings * @param Closure $callback * @return RouteGroup|IGroupRoute * @throws InvalidArgumentException */ public static function group(array $settings, Closure $callback): IGroupRoute { $group = new RouteGroup(); $group->setCallback($callback); $group->setSettings($settings); static::router()->addRoute($group); return $group; } /** * Special group that has the same benefits as group but supports * parameters and which are only rendered when the url matches. * * @param string $url * @param Closure $callback * @param array $settings * @return RoutePartialGroup|IPartialGroupRoute * @throws InvalidArgumentException */ public static function partialGroup(string $url, Closure $callback, array $settings = []): IPartialGroupRoute { $settings['prefix'] = $url; $group = new RoutePartialGroup(); $group->setSettings($settings); $group->setCallback($callback); static::router()->addRoute($group); return $group; } /** * Alias for the form method * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute * @see SimpleRouter::form */ public static function basic(string $url, $callback, array $settings = null): IRoute { return static::form($url, $callback, $settings); } /** * This type will route the given url to your callback on the provided request methods. * Route the given url to your callback on POST and GET request method. * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute * @see SimpleRouter::form */ public static function form(string $url, $callback, array $settings = null): IRoute { return static::match([ Request::REQUEST_TYPE_GET, Request::REQUEST_TYPE_POST, ], $url, $callback, $settings); } /** * This type will route the given url to your callback on the provided request methods. * * @param array $requestMethods * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function match(array $requestMethods, string $url, $callback, array $settings = null): IRoute { $route = new RouteUrl($url, $callback); $route->setRequestMethods($requestMethods); if ($settings !== null) { $route->setSettings($settings); } return static::router()->addRoute($route); } /** * This type will route the given url to your callback and allow any type of request method * * @param string $url * @param string|array|Closure $callback * @param array|null $settings * @return RouteUrl|IRoute */ public static function all(string $url, $callback, array $settings = null): IRoute { $route = new RouteUrl($url, $callback); if ($settings !== null) { $route->setSettings($settings); } return static::router()->addRoute($route); } /** * This route will route request from the given url to the controller. * * @param string $url * @param string $controller * @param array|null $settings * @return RouteController|IRoute */ public static function controller(string $url, string $controller, array $settings = null): IRoute { $route = new RouteController($url, $controller); if ($settings !== null) { $route->setSettings($settings); } return static::router()->addRoute($route); } /** * This type will route all REST-supported requests to different methods in the provided controller. * * @param string $url * @param string $controller * @param array|null $settings * @return RouteResource|IRoute */ public static function resource(string $url, string $controller, array $settings = null): IRoute { $route = new RouteResource($url, $controller); if ($settings !== null) { $route->setSettings($settings); } return static::router()->addRoute($route); } /** * Add exception callback handler. * * @param Closure $callback * @return CallbackExceptionHandler $callbackHandler */ public static function error(Closure $callback): CallbackExceptionHandler { $callbackHandler = new CallbackExceptionHandler($callback); static::router()->addExceptionHandler($callbackHandler); return $callbackHandler; } /** * Get url for a route by using either name/alias, class or method name. * * The name parameter supports the following values: * - Route name * - Controller/resource name (with or without method) * - Controller class name * * When searching for controller/resource by name, you can use this syntax "route.name@method". * You can also use the same syntax when searching for a specific controller-class "MyController@home". * If no arguments is specified, it will return the url for the current loaded route. * * @param string|null $name * @param string|array|null $parameters * @param array|null $getParams * @return Url */ public static function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url { try { return static::router()->getUrl($name, $parameters, $getParams); } catch (Exception $e) { return new Url('/'); } } /** * Get the request * * @return Request */ public static function request(): Request { return static::router()->getRequest(); } /** * Get the response object * * @return Response */ public static function response(): Response { if (static::$response === null) { static::$response = new Response(static::request()); } return static::$response; } /** * Returns the router instance * * @return Router */ public static function router(): Router { if (static::$router === null) { static::$router = new Router(); } return static::$router; } /** * Prepends the default namespace to all new routes added. * * @param ILoadableRoute|IRoute $route * @return IRoute */ public static function addDefaultNamespace(IRoute $route): IRoute { if (static::$defaultNamespace !== null) { $route->setNamespace(static::$defaultNamespace); } return $route; } /** * Changes the rendering behavior of the router. * When enabled the router will render all routes that matches. * When disabled the router will stop rendering at the first route that matches. * * @param bool $bool */ public static function enableMultiRouteRendering(bool $bool): void { static::router()->setRenderMultipleRoutes($bool); } /** * Set custom class-loader class used. * @param IClassLoader $classLoader */ public static function setCustomClassLoader(IClassLoader $classLoader): void { static::router()->setClassLoader($classLoader); } /** * Get default namespace * @return string|null */ public static function getDefaultNamespace(): ?string { return static::$defaultNamespace; } } Pecee/SimpleRouter/Event/IEventArgument.php 0000644 00000001317 15025064632 0014713 0 ustar 00 <?php namespace Pecee\SimpleRouter\Event; use Pecee\Http\Request; use Pecee\SimpleRouter\Router; interface IEventArgument { /** * Get event name * * @return string */ public function getEventName(): string; /** * Set event name * * @param string $name */ public function setEventName(string $name): void; /** * Get router instance * * @return Router */ public function getRouter(): Router; /** * Get request instance * * @return Request */ public function getRequest(): Request; /** * Get all event arguments * * @return array */ public function getArguments(): array; } Pecee/SimpleRouter/Event/EventArgument.php 0000644 00000003731 15025064632 0014604 0 ustar 00 <?php namespace Pecee\SimpleRouter\Event; use InvalidArgumentException; use Pecee\Http\Request; use Pecee\SimpleRouter\Router; class EventArgument implements IEventArgument { /** * Event name * @var string */ protected $eventName; /** * @var Router */ protected $router; /** * @var array */ protected $arguments = []; public function __construct(string $eventName, Router $router, array $arguments = []) { $this->eventName = $eventName; $this->router = $router; $this->arguments = $arguments; } /** * Get event name * * @return string */ public function getEventName(): string { return $this->eventName; } /** * Set the event name * * @param string $name */ public function setEventName(string $name): void { $this->eventName = $name; } /** * Get the router instance * * @return Router */ public function getRouter(): Router { return $this->router; } /** * Get the request instance * * @return Request */ public function getRequest(): Request { return $this->getRouter()->getRequest(); } /** * @param string $name * @return mixed */ public function __get(string $name) { return $this->arguments[$name] ?? null; } /** * @param string $name * @return bool */ public function __isset(string $name): bool { return array_key_exists($name, $this->arguments); } /** * @param string $name * @param mixed $value * @throws InvalidArgumentException */ public function __set(string $name, $value): void { throw new InvalidArgumentException('Not supported'); } /** * Get arguments * * @return array */ public function getArguments(): array { return $this->arguments; } } Pecee/SimpleRouter/Route/RouteUrl.php 0000644 00000002226 15025064632 0013614 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; class RouteUrl extends LoadableRoute { /** * RouteUrl constructor. * @param string $url * @param \Closure|string $callback */ public function __construct(string $url, $callback) { $this->setUrl($url); $this->setCallback($callback); } public function matchRoute(string $url, Request $request): bool { if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) { return false; } /* Match global regular-expression for route */ $regexMatch = $this->matchRegex($request, $url); if ($regexMatch === false) { return false; } /* Parse parameters from current route */ $parameters = $this->parseParameters($this->url, $url); /* If no custom regular expression or parameters was found on this route, we stop */ if ($regexMatch === null && $parameters === null) { return false; } /* Set the parameters */ $this->setParameters((array)$parameters); return true; } } Pecee/SimpleRouter/Route/IControllerRoute.php 0000644 00000000612 15025064632 0015303 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; interface IControllerRoute extends ILoadableRoute { /** * Get controller class-name * * @return string */ public function getController(): string; /** * Set controller class-name * * @param string $controller * @return static */ public function setController(string $controller): self; } Pecee/SimpleRouter/Route/RouteGroup.php 0000644 00000014453 15025064632 0014153 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; use Pecee\SimpleRouter\Handlers\IExceptionHandler; class RouteGroup extends Route implements IGroupRoute { protected $urlRegex = '/^%s\/?/u'; protected $prefix; protected $name; protected $domains = []; protected $exceptionHandlers = []; protected $mergeExceptionHandlers = true; /** * Method called to check if a domain matches * * @param Request $request * @return bool */ public function matchDomain(Request $request): bool { if ($this->domains === null || count($this->domains) === 0) { return true; } foreach ($this->domains as $domain) { // If domain has no parameters but matches if ($domain === $request->getHost()) { return true; } $parameters = $this->parseParameters($domain, $request->getHost(), '.*'); if ($parameters !== null && count($parameters) !== 0) { $this->parameters = $parameters; return true; } } return false; } /** * Method called to check if route matches * * @param string $url * @param Request $request * @return bool */ public function matchRoute(string $url, Request $request): bool { if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) { return false; } if ($this->prefix !== null) { /* Parse parameters from current route */ $parameters = $this->parseParameters($this->prefix, $url); /* If no custom regular expression or parameters was found on this route, we stop */ if ($parameters === null) { return false; } /* Set the parameters */ $this->setParameters($parameters); } $parsedPrefix = $this->prefix; foreach ($this->getParameters() as $parameter => $value) { $parsedPrefix = str_ireplace('{' . $parameter . '}', $value, $parsedPrefix); } /* Skip if prefix doesn't match */ if ($this->prefix !== null && stripos($url, rtrim($parsedPrefix, '/') . '/') === false) { return false; } return $this->matchDomain($request); } /** * Add exception handler * * @param IExceptionHandler|string $handler * @return static */ public function addExceptionHandler($handler): IGroupRoute { $this->exceptionHandlers[] = $handler; return $this; } /** * Set exception-handlers for group * * @param array $handlers * @return static */ public function setExceptionHandlers(array $handlers): IGroupRoute { $this->exceptionHandlers = $handlers; return $this; } /** * Get exception-handlers for group * * @return array */ public function getExceptionHandlers(): array { return $this->exceptionHandlers; } /** * Get allowed domains for domain. * * @return array */ public function getDomains(): array { return $this->domains; } /** * Set allowed domains for group. * * @param array $domains * @return static */ public function setDomains(array $domains): IGroupRoute { $this->domains = $domains; return $this; } /** * @param string $prefix * @return static */ public function setPrefix(string $prefix): IGroupRoute { $this->prefix = '/' . trim($prefix, '/'); return $this; } /** * Prepends prefix while ensuring that the url has the correct formatting. * * @param string $url * @return static */ public function prependPrefix(string $url): IGroupRoute { return $this->setPrefix(rtrim($url, '/') . $this->prefix); } /** * Set prefix that child-routes will inherit. * * @return string|null */ public function getPrefix(): ?string { return $this->prefix; } /** * When enabled group will overwrite any existing exception-handlers. * * @param bool $merge * @return static */ public function setMergeExceptionHandlers(bool $merge): IGroupRoute { $this->mergeExceptionHandlers = $merge; return $this; } /** * Returns true if group should overwrite existing exception-handlers. * * @return bool */ public function getMergeExceptionHandlers(): bool { return $this->mergeExceptionHandlers; } /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): IRoute { if (isset($settings['prefix']) === true) { $this->setPrefix($settings['prefix'] . $this->prefix); } if (isset($settings['mergeExceptionHandlers']) === true) { $this->setMergeExceptionHandlers($settings['mergeExceptionHandlers']); } if ($merge === false && isset($settings['exceptionHandler']) === true) { $this->setExceptionHandlers((array)$settings['exceptionHandler']); } if ($merge === false && isset($settings['domain']) === true) { $this->setDomains((array)$settings['domain']); } if (isset($settings['as']) === true) { $name = $settings['as']; if ($this->name !== null && $merge !== false) { $name .= '.' . $this->name; } $this->name = $name; } return parent::setSettings($settings, $merge); } /** * Export route settings to array so they can be merged with another route. * * @return array */ public function toArray(): array { $values = []; if ($this->prefix !== null) { $values['prefix'] = $this->getPrefix(); } if ($this->name !== null) { $values['as'] = $this->name; } if (count($this->parameters) !== 0) { $values['parameters'] = $this->parameters; } return array_merge($values, parent::toArray()); } } Pecee/SimpleRouter/Route/IRoute.php 0000644 00000011113 15025064632 0013235 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; use Pecee\SimpleRouter\Router; interface IRoute { /** * Method called to check if a domain matches * * @param string $url * @param Request $request * @return bool */ public function matchRoute(string $url, Request $request): bool; /** * Called when route is matched. * Returns class to be rendered. * * @param Request $request * @param Router $router * @return string * @throws \Pecee\SimpleRouter\Exceptions\NotFoundHttpException */ public function renderRoute(Request $request, Router $router): ?string; /** * Returns callback name/identifier for the current route based on the callback. * Useful if you need to get a unique identifier for the loaded route, for instance * when using translations etc. * * @return string */ public function getIdentifier(): string; /** * Set allowed request methods * * @param array $methods * @return static */ public function setRequestMethods(array $methods): self; /** * Get allowed request methods * * @return array */ public function getRequestMethods(): array; /** * @return IRoute|null */ public function getParent(): ?IRoute; /** * Get the group for the route. * * @return IGroupRoute|null */ public function getGroup(): ?IGroupRoute; /** * Set group * * @param IGroupRoute $group * @return static */ public function setGroup(IGroupRoute $group): self; /** * Set parent route * * @param IRoute $parent * @return static */ public function setParent(IRoute $parent): self; /** * Set callback * * @param string|array|\Closure $callback * @return static */ public function setCallback($callback): self; /** * @return string|callable */ public function getCallback(); /** * Return active method * * @return string|null */ public function getMethod(): ?string; /** * Set active method * * @param string $method * @return static */ public function setMethod(string $method): self; /** * Get class * * @return string|null */ public function getClass(): ?string; /** * @param string $namespace * @return static */ public function setNamespace(string $namespace): self; /** * @return string|null */ public function getNamespace(): ?string; /** * @param string $namespace * @return static */ public function setDefaultNamespace(string $namespace): IRoute; /** * Get default namespace * @return string|null */ public function getDefaultNamespace(): ?string; /** * Get parameter names. * * @return array */ public function getWhere(): array; /** * Set parameter names. * * @param array $options * @return static */ public function setWhere(array $options): self; /** * Get parameters * * @return array */ public function getParameters(): array; /** * Get parameters * * @param array $parameters * @return static */ public function setParameters(array $parameters): self; /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): self; /** * Export route settings to array so they can be merged with another route. * * @return array */ public function toArray(): array; /** * Get middlewares array * * @return array */ public function getMiddlewares(): array; /** * Set middleware class-name * * @param string $middleware * @return static */ public function addMiddleware(string $middleware): self; /** * Set middlewares array * * @param array $middlewares * @return static */ public function setMiddlewares(array $middlewares): self; /** * If enabled parameters containing null-value will not be passed along to the callback. * * @param bool $enabled * @return static $this */ public function setFilterEmptyParams(bool $enabled): self; /** * Status if filtering of empty params is enabled or disabled * @return bool */ public function getFilterEmptyParams(): bool; } Pecee/SimpleRouter/Route/Route.php 0000644 00000036372 15025064632 0013142 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; use Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException; use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; use Pecee\SimpleRouter\Router; abstract class Route implements IRoute { protected const PARAMETERS_REGEX_FORMAT = '%s([\w]+)(\%s?)%s'; protected const PARAMETERS_DEFAULT_REGEX = '[\w-]+'; /** * If enabled parameters containing null-value * will not be passed along to the callback. * * @var bool */ protected $filterEmptyParams = true; /** * Default regular expression used for parsing parameters. * @var string|null */ protected $defaultParameterRegex; protected $paramModifiers = '{}'; protected $paramOptionalSymbol = '?'; protected $urlRegex = '/^%s\/?$/u'; protected $group; protected $parent; /** * @var string|callable|null */ protected $callback; protected $defaultNamespace; /* Default options */ protected $namespace; protected $requestMethods = []; protected $where = []; protected $parameters = []; protected $originalParameters = []; protected $middlewares = []; /** * Render route * * @param Request $request * @param Router $router * @return string|null * @throws NotFoundHttpException */ public function renderRoute(Request $request, Router $router): ?string { $router->debug('Starting rendering route "%s"', get_class($this)); $callback = $this->getCallback(); if ($callback === null) { return null; } $router->debug('Parsing parameters'); $parameters = $this->getParameters(); $router->debug('Finished parsing parameters'); /* Filter parameters with null-value */ if ($this->filterEmptyParams === true) { $parameters = array_filter($parameters, static function ($var): bool { return ($var !== null); }); } /* Render callback function */ if (is_callable($callback) === true) { $router->debug('Executing callback'); /* Load class from type hinting */ if (is_array($callback) === true && isset($callback[0], $callback[1]) === true) { $callback[0] = $router->getClassLoader()->loadClass($callback[0]); } /* When the callback is a function */ return $router->getClassLoader()->loadClosure($callback, $parameters); } $controller = $this->getClass(); $method = $this->getMethod(); $namespace = $this->getNamespace(); $className = ($namespace !== null && $controller[0] !== '\\') ? $namespace . '\\' . $controller : $controller; $router->debug('Loading class %s', $className); $class = $router->getClassLoader()->loadClass($className); if ($method === null) { $controller[1] = '__invoke'; } if (method_exists($class, $method) === false) { throw new ClassNotFoundHttpException($className, $method, sprintf('Method "%s" does not exist in class "%s"', $method, $className), 404, null); } $router->debug('Executing callback %s -> %s', $className, $method); return $router->getClassLoader()->loadClassMethod($class, $method, $parameters); } protected function parseParameters($route, $url, $parameterRegex = null): ?array { $regex = (strpos($route, $this->paramModifiers[0]) === false) ? null : sprintf ( static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1] ); // Ensures that host names/domains will work with parameters $url = '/' . ltrim($url, '/'); $urlRegex = ''; $parameters = []; if ($regex === null || (bool)preg_match_all('/' . $regex . '/u', $route, $parameters) === false) { $urlRegex = preg_quote($route, '/'); } else { foreach (preg_split('/((-?\/?){[^}]+})/', $route) as $key => $t) { $regex = ''; if ($key < count($parameters[1])) { $name = $parameters[1][$key]; /* If custom regex is defined, use that */ if (isset($this->where[$name]) === true) { $regex = $this->where[$name]; } else { $regex = $parameterRegex ?? $this->defaultParameterRegex ?? static::PARAMETERS_DEFAULT_REGEX; } $regex = sprintf('((\/|-)(?P<%2$s>%3$s))%1$s', $parameters[2][$key], $name, $regex); } $urlRegex .= preg_quote($t, '/') . $regex; } } if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) { return null; } $values = []; if (isset($parameters[1]) === true) { $groupParameters = $this->getGroup() !== null ? $this->getGroup()->getParameters() : []; $lastParams = []; /* Only take matched parameters with name */ foreach ((array)$parameters[1] as $name) { // Ignore parent parameters if (isset($groupParameters[$name]) === true) { $lastParams[$name] = $matches[$name]; continue; } $values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null; } $values = array_merge($values, $lastParams); } $this->originalParameters = $values; return $values; } /** * Returns callback name/identifier for the current route based on the callback. * Useful if you need to get a unique identifier for the loaded route, for instance * when using translations etc. * * @return string */ public function getIdentifier(): string { if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { return $this->callback; } return 'function:' . md5($this->callback); } /** * Set allowed request methods * * @param array $methods * @return static */ public function setRequestMethods(array $methods): IRoute { $this->requestMethods = $methods; return $this; } /** * Get allowed request methods * * @return array */ public function getRequestMethods(): array { return $this->requestMethods; } /** * @return IRoute|null */ public function getParent(): ?IRoute { return $this->parent; } /** * Get the group for the route. * * @return IGroupRoute|null */ public function getGroup(): ?IGroupRoute { return $this->group; } /** * Set group * * @param IGroupRoute $group * @return static */ public function setGroup(IGroupRoute $group): IRoute { $this->group = $group; /* Add/merge parent settings with child */ return $this->setSettings($group->toArray(), true); } /** * Set parent route * * @param IRoute $parent * @return static */ public function setParent(IRoute $parent): IRoute { $this->parent = $parent; return $this; } /** * Set callback * * @param string|array|\Closure $callback * @return static */ public function setCallback($callback): IRoute { $this->callback = $callback; return $this; } /** * @return string|callable|null */ public function getCallback() { return $this->callback; } public function getMethod(): ?string { if (is_array($this->callback) === true && count($this->callback) > 1) { return $this->callback[1]; } if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); return $tmp[1]; } return null; } public function getClass(): ?string { if (is_array($this->callback) === true && count($this->callback) > 0) { return $this->callback[0]; } if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { $tmp = explode('@', $this->callback); return $tmp[0]; } return null; } public function setMethod(string $method): IRoute { $this->callback = [$this->getClass(), $method]; return $this; } public function setClass(string $class): IRoute { $this->callback = [$class, $this->getMethod()]; return $this; } /** * @param string $namespace * @return static */ public function setNamespace(string $namespace): IRoute { // Do not set namespace when class-hinting is used if (is_array($this->callback) === true) { return $this; } $ns = $this->getNamespace(); if ($ns !== null) { // Don't overwrite namespaces that starts with \ if ($ns[0] !== '\\') { $namespace .= '\\' . $ns; } else { $namespace = $ns; } } $this->namespace = $namespace; return $this; } /** * @param string $namespace * @return static */ public function setDefaultNamespace(string $namespace): IRoute { $this->defaultNamespace = $namespace; return $this; } public function getDefaultNamespace(): ?string { return $this->defaultNamespace; } /** * @return string|null */ public function getNamespace(): ?string { return $this->namespace ?? $this->defaultNamespace; } /** * Export route settings to array so they can be merged with another route. * * @return array */ public function toArray(): array { $values = []; if ($this->namespace !== null) { $values['namespace'] = $this->namespace; } if (count($this->requestMethods) !== 0) { $values['method'] = $this->requestMethods; } if (count($this->where) !== 0) { $values['where'] = $this->where; } if (count($this->middlewares) !== 0) { $values['middleware'] = $this->middlewares; } if ($this->defaultParameterRegex !== null) { $values['defaultParameterRegex'] = $this->defaultParameterRegex; } return $values; } /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): IRoute { if (isset($settings['namespace']) === true) { $this->setNamespace($settings['namespace']); } if (isset($settings['method']) === true) { $this->setRequestMethods(array_merge($this->requestMethods, (array)$settings['method'])); } if (isset($settings['where']) === true) { $this->setWhere(array_merge($this->where, (array)$settings['where'])); } if (isset($settings['parameters']) === true) { $this->setParameters(array_merge($this->parameters, (array)$settings['parameters'])); } // Push middleware if multiple if (isset($settings['middleware']) === true) { $this->setMiddlewares(array_merge((array)$settings['middleware'], $this->middlewares)); } if (isset($settings['defaultParameterRegex']) === true) { $this->setDefaultParameterRegex($settings['defaultParameterRegex']); } return $this; } /** * Get parameter names. * * @return array */ public function getWhere(): array { return $this->where; } /** * Set parameter names. * * @param array $options * @return static */ public function setWhere(array $options): IRoute { $this->where = $options; return $this; } /** * Add regular expression parameter match. * Alias for LoadableRoute::where() * * @param array $options * @return static * @see LoadableRoute::where() */ public function where(array $options) { return $this->setWhere($options); } /** * Get parameters * * @return array */ public function getParameters(): array { /* Sort the parameters after the user-defined param order, if any */ $parameters = []; if (count($this->originalParameters) !== 0) { $parameters = $this->originalParameters; } return array_merge($parameters, $this->parameters); } /** * Get parameters * * @param array $parameters * @return static */ public function setParameters(array $parameters): IRoute { $this->parameters = array_merge($this->parameters, $parameters); return $this; } /** * Add middleware class-name * * @param string $middleware * @return static * @deprecated This method is deprecated and will be removed in the near future. */ public function setMiddleware(string $middleware): self { $this->middlewares[] = $middleware; return $this; } /** * Add middleware class-name * * @param string $middleware * @return static */ public function addMiddleware(string $middleware): IRoute { $this->middlewares[] = $middleware; return $this; } /** * Set middlewares array * * @param array $middlewares * @return static */ public function setMiddlewares(array $middlewares): IRoute { $this->middlewares = $middlewares; return $this; } /** * @return array */ public function getMiddlewares(): array { return $this->middlewares; } /** * Set default regular expression used when matching parameters. * This is used when no custom parameter regex is found. * * @param string $regex * @return static */ public function setDefaultParameterRegex(string $regex): self { $this->defaultParameterRegex = $regex; return $this; } /** * Get default regular expression used when matching parameters. * * @return string */ public function getDefaultParameterRegex(): string { return $this->defaultParameterRegex; } /** * If enabled parameters containing null-value will not be passed along to the callback. * * @param bool $enabled * @return static $this */ public function setFilterEmptyParams(bool $enabled): IRoute { $this->filterEmptyParams = $enabled; return $this; } /** * Status if filtering of empty params is enabled or disabled * @return bool */ public function getFilterEmptyParams(): bool { return $this->filterEmptyParams; } } Pecee/SimpleRouter/Route/LoadableRoute.php 0000644 00000016505 15025064632 0014562 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Middleware\IMiddleware; use Pecee\Http\Request; use Pecee\SimpleRouter\Exceptions\HttpException; use Pecee\SimpleRouter\Router; abstract class LoadableRoute extends Route implements ILoadableRoute { /** * @var string */ protected $url; /** * @var string */ protected $name; /** * @var string|null */ protected $regex; /** * Loads and renders middlewares-classes * * @param Request $request * @param Router $router * @throws HttpException */ public function loadMiddleware(Request $request, Router $router): void { $router->debug('Loading middlewares'); foreach ($this->getMiddlewares() as $middleware) { if (is_object($middleware) === false) { $middleware = $router->getClassLoader()->loadClass($middleware); } if (($middleware instanceof IMiddleware) === false) { throw new HttpException($middleware . ' must be inherit the IMiddleware interface'); } $className = get_class($middleware); $router->debug('Loading middleware "%s"', $className); $middleware->handle($request); $router->debug('Finished loading middleware "%s"', $className); } $router->debug('Finished loading middlewares'); } public function matchRegex(Request $request, $url): ?bool { /* Match on custom defined regular expression */ if ($this->regex === null) { return null; } $parameters = []; if ((bool)preg_match($this->regex, $url, $parameters) !== false) { $this->setParameters($parameters); return true; } return false; } /** * Set url * * @param string $url * @return static */ public function setUrl(string $url): ILoadableRoute { $this->url = ($url === '/') ? '/' : '/' . trim($url, '/') . '/'; if (strpos($this->url, $this->paramModifiers[0]) !== false) { $regex = sprintf(static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1]); if ((bool)preg_match_all('/' . $regex . '/u', $this->url, $matches) !== false) { $this->parameters = array_fill_keys($matches[1], null); } } return $this; } /** * Prepends url while ensuring that the url has the correct formatting. * * @param string $url * @return ILoadableRoute */ public function prependUrl(string $url): ILoadableRoute { return $this->setUrl(rtrim($url, '/') . $this->url); } public function getUrl(): string { return $this->url; } /** * Returns true if group is defined and matches the given url. * * @param string $url * @param Request $request * @return bool */ protected function matchGroup(string $url, Request $request): bool { return ($this->getGroup() === null || $this->getGroup()->matchRoute($url, $request) === true); } /** * Find url that matches method, parameters or name. * Used when calling the url() helper. * * @param string|null $method * @param string|array|null $parameters * @param string|null $name * @return string */ public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string { $url = $this->getUrl(); $group = $this->getGroup(); if ($group !== null && count($group->getDomains()) !== 0) { $url = '//' . $group->getDomains()[0] . $url; } /* Create the param string - {parameter} */ $param1 = $this->paramModifiers[0] . '%s' . $this->paramModifiers[1]; /* Create the param string with the optional symbol - {parameter?} */ $param2 = $this->paramModifiers[0] . '%s' . $this->paramOptionalSymbol . $this->paramModifiers[1]; /* Replace any {parameter} in the url with the correct value */ $params = $this->getParameters(); foreach (array_keys($params) as $param) { if ($parameters === '' || (is_array($parameters) === true && count($parameters) === 0)) { $value = ''; } else { $p = (array)$parameters; $value = array_key_exists($param, $p) ? $p[$param] : $params[$param]; /* If parameter is specifically set to null - use the original-defined value */ if ($value === null && isset($this->originalParameters[$param]) === true) { $value = $this->originalParameters[$param]; } } if (stripos($url, $param1) !== false || stripos($url, $param) !== false) { /* Add parameter to the correct position */ $url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], $value, $url); } else { /* Parameter aren't recognized and will be appended at the end of the url */ $url .= $value . '/'; } } return rtrim('/' . ltrim($url, '/'), '/') . '/'; } /** * Returns the provided name for the router. * * @return string */ public function getName(): ?string { return $this->name; } /** * Check if route has given name. * * @param string $name * @return bool */ public function hasName(string $name): bool { return strtolower($this->name) === strtolower($name); } /** * Add regular expression match for the entire route. * * @param string $regex * @return static */ public function setMatch(string $regex): ILoadableRoute { $this->regex = $regex; return $this; } /** * Get regular expression match used for matching route (if defined). * * @return string */ public function getMatch(): string { return $this->regex; } /** * Sets the router name, which makes it easier to obtain the url or router at a later point. * Alias for LoadableRoute::setName(). * * @param string|array $name * @return static * @see LoadableRoute::setName() */ public function name($name): ILoadableRoute { return $this->setName($name); } /** * Sets the router name, which makes it easier to obtain the url or router at a later point. * * @param string $name * @return static */ public function setName(string $name): ILoadableRoute { $this->name = $name; return $this; } /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): IRoute { if (isset($settings['as']) === true) { $name = $settings['as']; if ($this->name !== null && $merge !== false) { $name .= '.' . $this->name; } $this->setName($name); } if (isset($settings['prefix']) === true) { $this->prependUrl($settings['prefix']); } return parent::setSettings($settings, $merge); } } Pecee/SimpleRouter/Route/IGroupRoute.php 0000644 00000003723 15025064632 0014262 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; use Pecee\SimpleRouter\Handlers\IExceptionHandler; interface IGroupRoute extends IRoute { /** * Method called to check if a domain matches * * @param Request $request * @return bool */ public function matchDomain(Request $request): bool; /** * Add exception handler * * @param IExceptionHandler|string $handler * @return static */ public function addExceptionHandler($handler): self; /** * Set exception-handlers for group * * @param array $handlers * @return static */ public function setExceptionHandlers(array $handlers): self; /** * Returns true if group should overwrite existing exception-handlers. * * @return bool */ public function getMergeExceptionHandlers(): bool; /** * When enabled group will overwrite any existing exception-handlers. * * @param bool $merge * @return static */ public function setMergeExceptionHandlers(bool $merge): self; /** * Get exception-handlers for group * * @return array */ public function getExceptionHandlers(): array; /** * Get domains for domain. * * @return array */ public function getDomains(): array; /** * Set allowed domains for group. * * @param array $domains * @return static */ public function setDomains(array $domains): self; /** * Prepends prefix while ensuring that the url has the correct formatting. * * @param string $url * @return static */ public function prependPrefix(string $url): self; /** * Set prefix that child-routes will inherit. * * @param string $prefix * @return static */ public function setPrefix(string $prefix): self; /** * Get prefix. * * @return string|null */ public function getPrefix(): ?string; } Pecee/SimpleRouter/Route/RouteController.php 0000644 00000010776 15025064632 0015206 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; class RouteController extends LoadableRoute implements IControllerRoute { protected $defaultMethod = 'index'; protected $controller; protected $method; protected $names = []; public function __construct($url, $controller) { $this->setUrl($url); $this->setName(trim(str_replace('/', '.', $url), '/')); $this->controller = $controller; } /** * Check if route has given name. * * @param string $name * @return bool */ public function hasName(string $name): bool { if ($this->name === null) { return false; } /* Remove method/type */ if (strpos($name, '.') !== false) { $method = substr($name, strrpos($name, '.') + 1); $newName = substr($name, 0, strrpos($name, '.')); if (in_array($method, $this->names, true) === true && strtolower($this->name) === strtolower($newName)) { return true; } } return parent::hasName($name); } /** * @param string|null $method * @param string|array|null $parameters * @param string|null $name * @return string */ public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string { if (strpos($name, '.') !== false) { $found = array_search(substr($name, strrpos($name, '.') + 1), $this->names, true); if ($found !== false) { $method = (string)$found; } } $url = ''; $parameters = (array)$parameters; if ($method !== null) { /* Remove requestType from method-name, if it exists */ foreach (Request::$requestTypes as $requestType) { if (stripos($method, $requestType) === 0) { $method = substr($method, strlen($requestType)); break; } } $method .= '/'; } $group = $this->getGroup(); if ($group !== null && count($group->getDomains()) !== 0) { $url .= '//' . $group->getDomains()[0]; } $url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower($method) . implode('/', $parameters); return '/' . trim($url, '/') . '/'; } public function matchRoute(string $url, Request $request): bool { if ($this->matchGroup($url, $request) === false) { return false; } /* Match global regular-expression for route */ $regexMatch = $this->matchRegex($request, $url); if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) { return false; } $strippedUrl = trim(str_ireplace($this->url, '/', $url), '/'); $path = explode('/', $strippedUrl); if (count($path) !== 0) { $method = (isset($path[0]) === false || trim($path[0]) === '') ? $this->defaultMethod : $path[0]; $this->method = $request->getMethod() . ucfirst($method); $this->parameters = array_slice($path, 1); // Set callback $this->setCallback([$this->controller, $this->method]); return true; } return false; } /** * Get controller class-name. * * @return string */ public function getController(): string { return $this->controller; } /** * Get controller class-name. * * @param string $controller * @return static */ public function setController(string $controller): IControllerRoute { $this->controller = $controller; return $this; } /** * Return active method * * @return string|null */ public function getMethod(): ?string { return $this->method; } /** * Set active method * * @param string $method * @return static */ public function setMethod(string $method): IRoute { $this->method = $method; return $this; } /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): IRoute { if (isset($settings['names']) === true) { $this->names = $settings['names']; } return parent::setSettings($settings, $merge); } } Pecee/SimpleRouter/Route/RoutePartialGroup.php 0000644 00000000170 15025064632 0015457 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; class RoutePartialGroup extends RouteGroup implements IPartialGroupRoute { } Pecee/SimpleRouter/Route/ILoadableRoute.php 0000644 00000003702 15025064632 0014666 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; use Pecee\SimpleRouter\Router; interface ILoadableRoute extends IRoute { /** * Find url that matches method, parameters or name. * Used when calling the url() helper. * * @param string|null $method * @param array|string|null $parameters * @param string|null $name * @return string */ public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string; /** * Loads and renders middleware-classes * * @param Request $request * @param Router $router */ public function loadMiddleware(Request $request, Router $router): void; /** * Get url * @return string */ public function getUrl(): string; /** * Set url * @param string $url * @return static */ public function setUrl(string $url): self; /** * Prepends url while ensuring that the url has the correct formatting. * @param string $url * @return ILoadableRoute */ public function prependUrl(string $url): self; /** * Returns the provided name for the router. * * @return string|null */ public function getName(): ?string; /** * Check if route has given name. * * @param string $name * @return bool */ public function hasName(string $name): bool; /** * Sets the router name, which makes it easier to obtain the url or router at a later point. * * @param string $name * @return static */ public function setName(string $name): self; /** * Get regular expression match used for matching route (if defined). * * @return string */ public function getMatch(): ?string; /** * Add regular expression match for the entire route. * * @param string $regex * @return static */ public function setMatch(string $regex): self; } Pecee/SimpleRouter/Route/IPartialGroupRoute.php 0000644 00000000115 15025064632 0015567 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; interface IPartialGroupRoute { } Pecee/SimpleRouter/Route/RouteResource.php 0000644 00000013505 15025064632 0014643 0 ustar 00 <?php namespace Pecee\SimpleRouter\Route; use Pecee\Http\Request; class RouteResource extends LoadableRoute implements IControllerRoute { protected $urls = [ 'index' => '', 'create' => 'create', 'store' => '', 'show' => '', 'edit' => 'edit', 'update' => '', 'destroy' => '', ]; protected $methodNames = [ 'index' => 'index', 'create' => 'create', 'store' => 'store', 'show' => 'show', 'edit' => 'edit', 'update' => 'update', 'destroy' => 'destroy', ]; protected $names = []; protected $controller; public function __construct($url, $controller) { $this->setUrl($url); $this->controller = $controller; $this->setName(trim(str_replace('/', '.', $url), '/')); } /** * Check if route has given name. * * @param string $name * @return bool */ public function hasName(string $name): bool { if ($this->name === null) { return false; } if (strtolower($this->name) === strtolower($name)) { return true; } /* Remove method/type */ if (strpos($name, '.') !== false) { $name = substr($name, 0, strrpos($name, '.')); } return (strtolower($this->name) === strtolower($name)); } /** * @param string|null $method * @param array|string|null $parameters * @param string|null $name * @return string */ public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string { $url = array_search($name, $this->names, true); if ($url !== false) { return rtrim($this->url . $this->urls[$url], '/') . '/'; } return $this->url; } protected function call($method): bool { $this->setCallback([$this->controller, $method]); return true; } public function matchRoute(string $url, Request $request): bool { if ($this->matchGroup($url, $request) === false) { return false; } /* Match global regular-expression for route */ $regexMatch = $this->matchRegex($request, $url); if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) { return false; } $route = rtrim($this->url, '/') . '/{id?}/{action?}'; /* Parse parameters from current route */ $this->parameters = $this->parseParameters($route, $url); /* If no custom regular expression or parameters was found on this route, we stop */ if ($regexMatch === null && $this->parameters === null) { return false; } $action = strtolower(trim($this->parameters['action'])); $id = $this->parameters['id']; // Remove action parameter unset($this->parameters['action']); $method = $request->getMethod(); // Delete if ($method === Request::REQUEST_TYPE_DELETE && $id !== null) { return $this->call($this->methodNames['destroy']); } // Update if ($id !== null && in_array($method, [Request::REQUEST_TYPE_PATCH, Request::REQUEST_TYPE_PUT], true) === true) { return $this->call($this->methodNames['update']); } // Edit if ($method === Request::REQUEST_TYPE_GET && $id !== null && $action === 'edit') { return $this->call($this->methodNames['edit']); } // Create if ($method === Request::REQUEST_TYPE_GET && $id === 'create') { return $this->call($this->methodNames['create']); } // Save if ($method === Request::REQUEST_TYPE_POST) { return $this->call($this->methodNames['store']); } // Show if ($method === Request::REQUEST_TYPE_GET && $id !== null) { return $this->call($this->methodNames['show']); } // Index return $this->call($this->methodNames['index']); } /** * @return string */ public function getController(): string { return $this->controller; } /** * @param string $controller * @return static */ public function setController(string $controller): IControllerRoute { $this->controller = $controller; return $this; } public function setName(string $name): ILoadableRoute { $this->name = $name; $this->names = [ 'index' => $this->name . '.index', 'create' => $this->name . '.create', 'store' => $this->name . '.store', 'show' => $this->name . '.show', 'edit' => $this->name . '.edit', 'update' => $this->name . '.update', 'destroy' => $this->name . '.destroy', ]; return $this; } /** * Define custom method name for resource controller * * @param array $names * @return static $this */ public function setMethodNames(array $names): RouteResource { $this->methodNames = $names; return $this; } /** * Get method names * * @return array */ public function getMethodNames(): array { return $this->methodNames; } /** * Merge with information from another route. * * @param array $settings * @param bool $merge * @return static */ public function setSettings(array $settings, bool $merge = false): IRoute { if (isset($settings['names']) === true) { $this->names = $settings['names']; } if (isset($settings['methods']) === true) { $this->methodNames = $settings['methods']; } return parent::setSettings($settings, $merge); } } Pecee/SimpleRouter/Exceptions/HttpException.php 0000644 00000000153 15025064632 0015651 0 ustar 00 <?php namespace Pecee\SimpleRouter\Exceptions; use Exception; class HttpException extends Exception { } Pecee/SimpleRouter/Exceptions/ClassNotFoundHttpException.php 0000644 00000001452 15025064632 0020317 0 ustar 00 <?php namespace Pecee\SimpleRouter\Exceptions; use Throwable; class ClassNotFoundHttpException extends NotFoundHttpException { /** * @var string */ protected $class; /** * @var string|null */ protected $method; public function __construct(string $class, ?string $method = null, string $message = "", int $code = 0, Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->class = $class; $this->method = $method; } /** * Get class name * @return string */ public function getClass(): string { return $this->class; } /** * Get method * @return string|null */ public function getMethod(): ?string { return $this->method; } } Pecee/SimpleRouter/Exceptions/NotFoundHttpException.php 0000644 00000000147 15025064632 0017331 0 ustar 00 <?php namespace Pecee\SimpleRouter\Exceptions; class NotFoundHttpException extends HttpException { } Pecee/SimpleRouter/IRouterBootManager.php 0000644 00000000440 15025064632 0014441 0 ustar 00 <?php namespace Pecee\SimpleRouter; use Pecee\Http\Request; interface IRouterBootManager { /** * Called when router loads it's routes * * @param Router $router * @param Request $request */ public function boot(Router $router, Request $request): void; } Pecee/SimpleRouter/ClassLoader/IClassLoader.php 0000644 00000001255 15025064632 0015437 0 ustar 00 <?php namespace Pecee\SimpleRouter\ClassLoader; interface IClassLoader { /** * Called when loading class * @param string $class * @return object */ public function loadClass(string $class); /** * Called when loading class method * @param object $class * @param string $method * @param array $parameters * @return object */ public function loadClassMethod($class, string $method, array $parameters); /** * Called when loading method * * @param callable $closure * @param array $parameters * @return mixed */ public function loadClosure(Callable $closure, array $parameters); } Pecee/SimpleRouter/ClassLoader/ClassLoader.php 0000644 00000002237 15025064632 0015327 0 ustar 00 <?php namespace Pecee\SimpleRouter\ClassLoader; use Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException; class ClassLoader implements IClassLoader { /** * Load class * * @param string $class * @return object * @throws ClassNotFoundHttpException */ public function loadClass(string $class) { if (class_exists($class) === false) { throw new ClassNotFoundHttpException($class, null, sprintf('Class "%s" does not exist', $class), 404, null); } return new $class(); } /** * Called when loading class method * @param object $class * @param string $method * @param array $parameters * @return object */ public function loadClassMethod($class, string $method, array $parameters) { return call_user_func_array([$class, $method], array_values($parameters)); } /** * Load closure * * @param Callable $closure * @param array $parameters * @return mixed */ public function loadClosure(Callable $closure, array $parameters) { return call_user_func_array($closure, array_values($parameters)); } } Pecee/SimpleRouter/Router.php 0000644 00000066335 15025064632 0012230 0 ustar 00 <?php namespace Pecee\SimpleRouter; use Exception; use Pecee\Exceptions\InvalidArgumentException; use Pecee\Http\Exceptions\MalformedUrlException; use Pecee\Http\Middleware\BaseCsrfVerifier; use Pecee\Http\Request; use Pecee\Http\Url; use Pecee\SimpleRouter\ClassLoader\ClassLoader; use Pecee\SimpleRouter\ClassLoader\IClassLoader; use Pecee\SimpleRouter\Exceptions\HttpException; use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; use Pecee\SimpleRouter\Handlers\EventHandler; use Pecee\SimpleRouter\Handlers\IEventHandler; use Pecee\SimpleRouter\Handlers\IExceptionHandler; use Pecee\SimpleRouter\Route\IControllerRoute; use Pecee\SimpleRouter\Route\IGroupRoute; use Pecee\SimpleRouter\Route\ILoadableRoute; use Pecee\SimpleRouter\Route\IPartialGroupRoute; use Pecee\SimpleRouter\Route\IRoute; class Router { /** * Current request * @var Request */ protected $request; /** * Defines if a route is currently being processed. * @var bool */ protected $isProcessingRoute; /** * All added routes * @var array */ protected $routes = []; /** * List of processed routes * @var array|ILoadableRoute[] */ protected $processedRoutes = []; /** * Stack of routes used to keep track of sub-routes added * when a route is being processed. * @var array */ protected $routeStack = []; /** * List of added bootmanagers * @var array */ protected $bootManagers = []; /** * Csrf verifier class * @var BaseCsrfVerifier|null */ protected $csrfVerifier; /** * Get exception handlers * @var array */ protected $exceptionHandlers = []; /** * List of loaded exception that has been loaded. * Used to ensure that exception-handlers aren't loaded twice when rewriting route. * * @var array */ protected $loadedExceptionHandlers = []; /** * Enable or disabled debugging * @var bool */ protected $debugEnabled = false; /** * The start time used when debugging is enabled * @var float */ protected $debugStartTime; /** * List containing all debug messages * @var array */ protected $debugList = []; /** * Contains any registered event-handler. * @var array */ protected $eventHandlers = []; /** * Class loader instance * @var IClassLoader */ protected $classLoader; /** * When enabled the router will render all routes that matches. * When disabled the router will stop execution when first route is found. * @var bool */ protected $renderMultipleRoutes = true; /** * Router constructor. */ public function __construct() { $this->reset(); } /** * Resets the router by reloading request and clearing all routes and data. */ public function reset(): void { $this->debugStartTime = microtime(true); $this->isProcessingRoute = false; try { $this->request = new Request(); } catch (MalformedUrlException $e) { $this->debug(sprintf('Invalid request-uri url: %s', $e->getMessage())); } $this->routes = []; $this->bootManagers = []; $this->routeStack = []; $this->processedRoutes = []; $this->exceptionHandlers = []; $this->loadedExceptionHandlers = []; $this->eventHandlers = []; $this->debugList = []; $this->csrfVerifier = null; $this->classLoader = new ClassLoader(); } /** * Add route * @param IRoute $route * @return IRoute */ public function addRoute(IRoute $route): IRoute { $this->fireEvents(EventHandler::EVENT_ADD_ROUTE, [ 'route' => $route, 'isSubRoute' => $this->isProcessingRoute, ]); /* * If a route is currently being processed, that means that the route being added are rendered from the parent * routes callback, so we add them to the stack instead. */ if ($this->isProcessingRoute === true) { $this->routeStack[] = $route; } else { $this->routes[] = $route; } return $route; } /** * Render and process any new routes added. * * @param IRoute $route * @throws NotFoundHttpException */ protected function renderAndProcess(IRoute $route): void { $this->isProcessingRoute = true; $route->renderRoute($this->request, $this); $this->isProcessingRoute = false; if (count($this->routeStack) !== 0) { /* Pop and grab the routes added when executing group callback earlier */ $stack = $this->routeStack; $this->routeStack = []; /* Route any routes added to the stack */ $this->processRoutes($stack, ($route instanceof IGroupRoute) ? $route : null); } } /** * Process added routes. * * @param array|IRoute[] $routes * @param IGroupRoute|null $group * @throws NotFoundHttpException */ protected function processRoutes(array $routes, ?IGroupRoute $group = null): void { $this->debug('Processing routes'); // Stop processing routes if no valid route is found. if ($this->request->getRewriteRoute() === null && $this->request->getUrl()->getOriginalUrl() === '') { $this->debug('Halted route-processing as no valid route was found'); return; } $url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath(); // Loop through each route-request foreach ($routes as $route) { $this->debug('Processing route "%s"', get_class($route)); if ($group !== null) { /* Add the parent group */ $route->setGroup($group); } /* @var $route IGroupRoute */ if ($route instanceof IGroupRoute) { if ($route->matchRoute($url, $this->request) === true) { /* Add exception handlers */ if (count($route->getExceptionHandlers()) !== 0) { if ($route->getMergeExceptionHandlers() === true) { foreach ($route->getExceptionHandlers() as $handler) { $this->exceptionHandlers[] = $handler; } } else { $this->exceptionHandlers = $route->getExceptionHandlers(); } } /* Only render partial group if it matches */ if ($route instanceof IPartialGroupRoute === true) { $this->renderAndProcess($route); continue; } } if ($route instanceof IPartialGroupRoute === false) { $this->renderAndProcess($route); } continue; } if ($route instanceof ILoadableRoute === true) { /* Add the route to the map, so we can find the active one when all routes has been loaded */ $this->processedRoutes[] = $route; } } } /** * Load routes * @return void * @throws NotFoundHttpException */ public function loadRoutes(): void { $this->debug('Loading routes'); $this->fireEvents(EventHandler::EVENT_LOAD_ROUTES, [ 'routes' => $this->routes, ]); /* Loop through each route-request */ $this->processRoutes($this->routes); $this->fireEvents(EventHandler::EVENT_BOOT, [ 'bootmanagers' => $this->bootManagers, ]); /* Initialize boot-managers */ /* @var $manager IRouterBootManager */ foreach ($this->bootManagers as $manager) { $className = get_class($manager); $this->debug('Rendering bootmanager "%s"', $className); $this->fireEvents(EventHandler::EVENT_RENDER_BOOTMANAGER, [ 'bootmanagers' => $this->bootManagers, 'bootmanager' => $manager, ]); /* Render bootmanager */ $manager->boot($this, $this->request); $this->debug('Finished rendering bootmanager "%s"', $className); } $this->debug('Finished loading routes'); } /** * Start the routing * * @return string|null * @throws NotFoundHttpException * @throws \Pecee\Http\Middleware\Exceptions\TokenMismatchException * @throws HttpException * @throws Exception */ public function start(): ?string { $this->debug('Router starting'); $this->fireEvents(EventHandler::EVENT_INIT); $this->loadRoutes(); if ($this->csrfVerifier !== null) { $this->fireEvents(EventHandler::EVENT_RENDER_CSRF, [ 'csrfVerifier' => $this->csrfVerifier, ]); /* Verify csrf token for request */ $this->csrfVerifier->handle($this->request); } $output = $this->routeRequest(); $this->fireEvents(EventHandler::EVENT_LOAD, [ 'loadedRoutes' => $this->getRequest()->getLoadedRoutes(), ]); $this->debug('Routing complete'); return $output; } /** * Routes the request * * @return string|null * @throws HttpException * @throws Exception */ public function routeRequest(): ?string { $this->debug('Routing request'); $methodNotAllowed = null; try { $url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath(); /* @var $route ILoadableRoute */ foreach ($this->processedRoutes as $key => $route) { $this->debug('Matching route "%s"', get_class($route)); /* If the route matches */ if ($route->matchRoute($url, $this->request) === true) { $this->fireEvents(EventHandler::EVENT_MATCH_ROUTE, [ 'route' => $route, ]); /* Check if request method matches */ if (count($route->getRequestMethods()) !== 0 && in_array($this->request->getMethod(), $route->getRequestMethods(), true) === false) { $this->debug('Method "%s" not allowed', $this->request->getMethod()); // Only set method not allowed is not already set if ($methodNotAllowed === null) { $methodNotAllowed = true; } continue; } $this->fireEvents(EventHandler::EVENT_RENDER_MIDDLEWARES, [ 'route' => $route, 'middlewares' => $route->getMiddlewares(), ]); $route->loadMiddleware($this->request, $this); $output = $this->handleRouteRewrite($key, $url); if ($output !== null) { return $output; } $methodNotAllowed = false; $this->request->addLoadedRoute($route); $this->fireEvents(EventHandler::EVENT_RENDER_ROUTE, [ 'route' => $route, ]); $routeOutput = $route->renderRoute($this->request, $this); if ($this->renderMultipleRoutes === true) { if ($routeOutput !== null) { return $routeOutput; } $output = $this->handleRouteRewrite($key, $url); if ($output !== null) { return $output; } } else { $output = $this->handleRouteRewrite($key, $url); return $output ?? $routeOutput; } } } } catch (Exception $e) { $this->handleException($e); } if ($methodNotAllowed === true) { $message = sprintf('Route "%s" or method "%s" not allowed.', $this->request->getUrl()->getPath(), $this->request->getMethod()); $this->handleException(new NotFoundHttpException($message, 403)); } if (count($this->request->getLoadedRoutes()) === 0) { $rewriteUrl = $this->request->getRewriteUrl(); if ($rewriteUrl !== null) { $message = sprintf('Route not found: "%s" (rewrite from: "%s")', $rewriteUrl, $this->request->getUrl()->getPath()); } else { $message = sprintf('Route not found: "%s"', $this->request->getUrl()->getPath()); } $this->debug($message); return $this->handleException(new NotFoundHttpException($message, 404)); } return null; } /** * Handle route-rewrite * * @param string $key * @param string $url * @return string|null * @throws HttpException * @throws Exception */ protected function handleRouteRewrite(string $key, string $url): ?string { /* If the request has changed */ if ($this->request->hasPendingRewrite() === false) { return null; } $route = $this->request->getRewriteRoute(); if ($route !== null) { /* Add rewrite route */ $this->processedRoutes[] = $route; } if ($this->request->getRewriteUrl() !== $url) { unset($this->processedRoutes[$key]); $this->request->setHasPendingRewrite(false); $this->fireEvents(EventHandler::EVENT_REWRITE, [ 'rewriteUrl' => $this->request->getRewriteUrl(), 'rewriteRoute' => $this->request->getRewriteRoute(), ]); return $this->routeRequest(); } return null; } /** * @param Exception $e * @return string|null * @throws Exception * @throws HttpException */ protected function handleException(Exception $e): ?string { $this->debug('Starting exception handling for "%s"', get_class($e)); $this->fireEvents(EventHandler::EVENT_LOAD_EXCEPTIONS, [ 'exception' => $e, 'exceptionHandlers' => $this->exceptionHandlers, ]); /* @var $handler IExceptionHandler */ foreach (array_reverse($this->exceptionHandlers) as $key => $handler) { if (is_object($handler) === false) { $handler = new $handler(); } $this->fireEvents(EventHandler::EVENT_RENDER_EXCEPTION, [ 'exception' => $e, 'exceptionHandler' => $handler, 'exceptionHandlers' => $this->exceptionHandlers, ]); $this->debug('Processing exception-handler "%s"', get_class($handler)); if (($handler instanceof IExceptionHandler) === false) { throw new HttpException('Exception handler must implement the IExceptionHandler interface.', 500); } try { $this->debug('Start rendering exception handler'); $handler->handleError($this->request, $e); $this->debug('Finished rendering exception-handler'); if (isset($this->loadedExceptionHandlers[$key]) === false && $this->request->hasPendingRewrite() === true) { $this->loadedExceptionHandlers[$key] = $handler; $this->debug('Exception handler contains rewrite, reloading routes'); $this->fireEvents(EventHandler::EVENT_REWRITE, [ 'rewriteUrl' => $this->request->getRewriteUrl(), 'rewriteRoute' => $this->request->getRewriteRoute(), ]); if ($this->request->getRewriteRoute() !== null) { $this->processedRoutes[] = $this->request->getRewriteRoute(); } return $this->routeRequest(); } } catch (Exception $e) { } $this->debug('Finished processing'); } $this->debug('Finished exception handling - exception not handled, throwing'); throw $e; } /** * Find route by alias, class, callback or method. * * @param string $name * @return ILoadableRoute|null */ public function findRoute(string $name): ?ILoadableRoute { $this->debug('Finding route by name "%s"', $name); $this->fireEvents(EventHandler::EVENT_FIND_ROUTE, [ 'name' => $name, ]); foreach ($this->processedRoutes as $route) { /* Check if the name matches with a name on the route. Should match either router alias or controller alias. */ if ($route->hasName($name) === true) { $this->debug('Found route "%s" by name "%s"', $route->getUrl(), $name); return $route; } /* Direct match to controller */ if ($route instanceof IControllerRoute && strtoupper($route->getController()) === strtoupper($name)) { $this->debug('Found route "%s" by controller "%s"', $route->getUrl(), $name); return $route; } /* Using @ is most definitely a controller@method or alias@method */ if (strpos($name, '@') !== false) { [$controller, $method] = array_map('strtolower', explode('@', $name)); if ($controller === strtolower($route->getClass()) && $method === strtolower($route->getMethod())) { $this->debug('Found route "%s" by controller "%s" and method "%s"', $route->getUrl(), $controller, $method); return $route; } } /* Check if callback matches (if it's not a function) */ $callback = $route->getCallback(); if (is_string($callback) === true && is_callable($callback) === false && strpos($name, '@') !== false && strpos($callback, '@') !== false) { /* Check if the entire callback is matching */ if (strpos($callback, $name) === 0 || strtolower($callback) === strtolower($name)) { $this->debug('Found route "%s" by callback "%s"', $route->getUrl(), $name); return $route; } /* Check if the class part of the callback matches (class@method) */ if (strtolower($name) === strtolower($route->getClass())) { $this->debug('Found route "%s" by class "%s"', $route->getUrl(), $name); return $route; } } } $this->debug('Route not found'); return null; } /** * Get url for a route by using either name/alias, class or method name. * * The name parameter supports the following values: * - Route name * - Controller/resource name (with or without method) * - Controller class name * * When searching for controller/resource by name, you can use this syntax "route.name@method". * You can also use the same syntax when searching for a specific controller-class "MyController@home". * If no arguments is specified, it will return the url for the current loaded route. * * @param string|null $name * @param string|array|null $parameters * @param array|null $getParams * @return Url * @throws InvalidArgumentException */ public function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url { $this->debug('Finding url', func_get_args()); $this->fireEvents(EventHandler::EVENT_GET_URL, [ 'name' => $name, 'parameters' => $parameters, 'getParams' => $getParams, ]); if ($name === '' && $parameters === '') { return new Url('/'); } /* Only merge $_GET when all parameters are null */ $getParams = ($name === null && $parameters === null && $getParams === null) ? $_GET : (array)$getParams; /* Return current route if no options has been specified */ if ($name === null && $parameters === null) { return $this->request ->getUrlCopy() ->setParams($getParams); } $loadedRoute = $this->request->getLoadedRoute(); /* If nothing is defined and a route is loaded we use that */ if ($name === null && $loadedRoute !== null) { return $this->request ->getUrlCopy() ->setPath($loadedRoute->findUrl($loadedRoute->getMethod(), $parameters, $name)) ->setParams($getParams); } if ($name !== null) { /* We try to find a match on the given name */ $route = $this->findRoute($name); if ($route !== null) { return $this->request ->getUrlCopy() ->setPath($route->findUrl($route->getMethod(), $parameters, $name)) ->setParams($getParams); } } /* Using @ is most definitely a controller@method or alias@method */ if (is_string($name) === true && strpos($name, '@') !== false) { [$controller, $method] = explode('@', $name); /* Loop through all the routes to see if we can find a match */ /* @var $route ILoadableRoute */ foreach ($this->processedRoutes as $processedRoute) { /* Check if the route contains the name/alias */ if ($processedRoute->hasName($controller) === true) { return $this->request ->getUrlCopy() ->setPath($processedRoute->findUrl($method, $parameters, $name)) ->setParams($getParams); } /* Check if the route controller is equal to the name */ if ($processedRoute instanceof IControllerRoute && strtolower($processedRoute->getController()) === strtolower($controller)) { return $this->request ->getUrlCopy() ->setPath($processedRoute->findUrl($method, $parameters, $name)) ->setParams($getParams); } } } /* No result so we assume that someone is using a hardcoded url and join everything together. */ $url = trim(implode('/', array_merge((array)$name, (array)$parameters)), '/'); $url = (($url === '') ? '/' : '/' . $url . '/'); return $this->request ->getUrlCopy() ->setPath($url) ->setParams($getParams); } /** * Get BootManagers * @return array */ public function getBootManagers(): array { return $this->bootManagers; } /** * Set BootManagers * * @param array $bootManagers * @return static */ public function setBootManagers(array $bootManagers): self { $this->bootManagers = $bootManagers; return $this; } /** * Add BootManager * * @param IRouterBootManager $bootManager * @return static */ public function addBootManager(IRouterBootManager $bootManager): self { $this->bootManagers[] = $bootManager; return $this; } /** * Get routes that has been processed. * * @return array */ public function getProcessedRoutes(): array { return $this->processedRoutes; } /** * @return array */ public function getRoutes(): array { return $this->routes; } /** * Set routes * * @param array $routes * @return static */ public function setRoutes(array $routes): self { $this->routes = $routes; return $this; } /** * Get current request * * @return Request */ public function getRequest(): Request { return $this->request; } /** * Get csrf verifier class * @return BaseCsrfVerifier */ public function getCsrfVerifier(): ?BaseCsrfVerifier { return $this->csrfVerifier; } /** * Set csrf verifier class * * @param BaseCsrfVerifier $csrfVerifier */ public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier): void { $this->csrfVerifier = $csrfVerifier; } /** * Set class loader * * @param IClassLoader $loader */ public function setClassLoader(IClassLoader $loader): void { $this->classLoader = $loader; } /** * Get class loader * * @return IClassLoader */ public function getClassLoader(): IClassLoader { return $this->classLoader; } /** * Register event handler * * @param IEventHandler $handler */ public function addEventHandler(IEventHandler $handler): void { $this->eventHandlers[] = $handler; } /** * Get registered event-handler. * * @return array */ public function getEventHandlers(): array { return $this->eventHandlers; } /** * Fire event in event-handler. * * @param string $name * @param array $arguments */ protected function fireEvents(string $name, array $arguments = []): void { if (count($this->eventHandlers) === 0) { return; } /* @var IEventHandler $eventHandler */ foreach ($this->eventHandlers as $eventHandler) { $eventHandler->fireEvents($this, $name, $arguments); } } /** * Add new debug message * @param string $message * @param array $args */ public function debug(string $message, ...$args): void { if ($this->debugEnabled === false) { return; } $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); $this->debugList[] = [ 'message' => vsprintf($message, $args), 'time' => number_format(microtime(true) - $this->debugStartTime, 10), 'trace' => end($trace), ]; } /** * Enable or disables debugging * * @param bool $enabled * @return static */ public function setDebugEnabled(bool $enabled): self { $this->debugEnabled = $enabled; return $this; } /** * Get the list containing all debug messages. * * @return array */ public function getDebugLog(): array { return $this->debugList; } /** * Changes the rendering behavior of the router. * When enabled the router will render all routes that matches. * When disabled the router will stop rendering at the first route that matches. * * @param bool $bool * @return $this */ public function setRenderMultipleRoutes(bool $bool): self { $this->renderMultipleRoutes = $bool; return $this; } public function addExceptionHandler(IExceptionHandler $handler): self { $this->exceptionHandlers[] = $handler; return $this; } } Pecee/Controllers/IResourceController.php 0000644 00000001226 15025064632 0014554 0 ustar 00 <?php namespace Pecee\Controllers; interface IResourceController { /** * @return mixed */ public function index(); /** * @param mixed $id * @return mixed */ public function show($id); /** * @return mixed */ public function store(); /** * @return mixed */ public function create(); /** * View * @param mixed $id * @return mixed */ public function edit($id); /** * @param mixed $id * @return mixed */ public function update($id); /** * @param mixed $id * @return mixed */ public function destroy($id); } Pecee/Http/Url.php 0000644 00000025024 15025064632 0007765 0 ustar 00 <?php namespace Pecee\Http; use JsonSerializable; use Pecee\Http\Exceptions\MalformedUrlException; class Url implements JsonSerializable { /** * @var string|null */ private $originalUrl; /** * @var string|null */ private $scheme; /** * @var string|null */ private $host; /** * @var int|null */ private $port; /** * @var string|null */ private $username; /** * @var string|null */ private $password; /** * @var string|null */ private $path; /** * @var array */ private $params = []; /** * @var string|null */ private $fragment; /** * Url constructor. * * @param ?string $url * @throws MalformedUrlException */ public function __construct(?string $url) { $this->originalUrl = $url; if ($url !== null && $url !== '/') { $data = $this->parseUrl($url); $this->scheme = $data['scheme'] ?? null; $this->host = $data['host'] ?? null; $this->port = $data['port'] ?? null; $this->username = $data['user'] ?? null; $this->password = $data['pass'] ?? null; if (isset($data['path']) === true) { $this->setPath($data['path']); } $this->fragment = $data['fragment'] ?? null; if (isset($data['query']) === true) { $this->setQueryString($data['query']); } } } /** * Check if url is using a secure protocol like https * * @return bool */ public function isSecure(): bool { return (strtolower($this->getScheme()) === 'https'); } /** * Checks if url is relative * * @return bool */ public function isRelative(): bool { return ($this->getHost() === null); } /** * Get url scheme * * @return string|null */ public function getScheme(): ?string { return $this->scheme; } /** * Set the scheme of the url * * @param string $scheme * @return static */ public function setScheme(string $scheme): self { $this->scheme = $scheme; return $this; } /** * Get url host * * @return string|null */ public function getHost(): ?string { return $this->host; } /** * Set the host of the url * * @param string $host * @return static */ public function setHost(string $host): self { $this->host = $host; return $this; } /** * Get url port * * @return int|null */ public function getPort(): ?int { return ($this->port !== null) ? (int)$this->port : null; } /** * Set the port of the url * * @param int $port * @return static */ public function setPort(int $port): self { $this->port = $port; return $this; } /** * Parse username from url * * @return string|null */ public function getUsername(): ?string { return $this->username; } /** * Set the username of the url * * @param string $username * @return static */ public function setUsername(string $username): self { $this->username = $username; return $this; } /** * Parse password from url * @return string|null */ public function getPassword(): ?string { return $this->password; } /** * Set the url password * * @param string $password * @return static */ public function setPassword(string $password): self { $this->password = $password; return $this; } /** * Get path from url * @return string */ public function getPath(): ?string { return $this->path ?? '/'; } /** * Set the url path * * @param string $path * @return static */ public function setPath(string $path): self { $this->path = rtrim($path, '/') . '/'; return $this; } /** * Get query-string from url * * @return array */ public function getParams(): array { return $this->params; } /** * Merge parameters array * * @param array $params * @return static */ public function mergeParams(array $params): self { return $this->setParams(array_merge($this->getParams(), $params)); } /** * Set the url params * * @param array $params * @return static */ public function setParams(array $params): self { $this->params = $params; return $this; } /** * Set raw query-string parameters as string * * @param string $queryString * @return static */ public function setQueryString(string $queryString): self { $params = []; parse_str($queryString, $params); if(count($params) > 0) { return $this->setParams($params); } return $this; } /** * Get query-string params as string * * @return string */ public function getQueryString(): string { return static::arrayToParams($this->getParams()); } /** * Get fragment from url (everything after #) * * @return string|null */ public function getFragment(): ?string { return $this->fragment; } /** * Set url fragment * * @param string $fragment * @return static */ public function setFragment(string $fragment): self { $this->fragment = $fragment; return $this; } /** * @return string */ public function getOriginalUrl(): string { return $this->originalUrl; } /** * Get position of value. * Returns -1 on failure. * * @param string $value * @return int */ public function indexOf(string $value): int { $index = stripos($this->getOriginalUrl(), $value); return ($index === false) ? -1 : $index; } /** * Check if url contains value. * * @param string $value * @return bool */ public function contains(string $value): bool { return (stripos($this->getOriginalUrl(), $value) !== false); } /** * Check if url contains parameter/query string. * * @param string $name * @return bool */ public function hasParam(string $name): bool { return array_key_exists($name, $this->getParams()); } /** * Removes multiple parameters from the query-string * * @param array ...$names * @return static */ public function removeParams(...$names): self { $params = array_diff_key($this->getParams(), array_flip(...$names)); $this->setParams($params); return $this; } /** * Removes parameter from the query-string * * @param string $name * @return static */ public function removeParam(string $name): self { $params = $this->getParams(); unset($params[$name]); $this->setParams($params); return $this; } /** * Get parameter by name. * Returns parameter value or default value. * * @param string $name * @param string|null $defaultValue * @return string|null */ public function getParam(string $name, ?string $defaultValue = null): ?string { return (isset($this->getParams()[$name]) === true) ? $this->getParams()[$name] : $defaultValue; } /** * UTF-8 aware parse_url() replacement. * @param string $url * @param int $component * @return array * @throws MalformedUrlException */ public function parseUrl(string $url, int $component = -1): array { $encodedUrl = preg_replace_callback( '/[^:\/@?&=#]+/u', static function ($matches): string { return urlencode($matches[0]); }, $url ); $parts = parse_url($encodedUrl, $component); if ($parts === false) { throw new MalformedUrlException(sprintf('Failed to parse url: "%s"', $url)); } return array_map('urldecode', $parts); } /** * Convert array to query-string params * * @param array $getParams * @param bool $includeEmpty * @return string */ public static function arrayToParams(array $getParams = [], bool $includeEmpty = true): string { if (count($getParams) !== 0) { if ($includeEmpty === false) { $getParams = array_filter($getParams, static function ($item): bool { return (trim($item) !== ''); }); } return http_build_query($getParams); } return ''; } /** * Returns the relative url * * @param bool $includeParams * @return string */ public function getRelativeUrl(bool $includeParams = true): string { $path = $this->path ?? '/'; if($includeParams === false) { return $path; } $query = $this->getQueryString() !== '' ? '?' . $this->getQueryString() : ''; $fragment = $this->fragment !== null ? '#' . $this->fragment : ''; return $path . $query . $fragment; } /** * Returns the absolute url * * @param bool $includeParams * @return string */ public function getAbsoluteUrl(bool $includeParams = true): string { $scheme = $this->scheme !== null ? $this->scheme . '://' : ''; $host = $this->host ?? ''; $port = $this->port !== null ? ':' . $this->port : ''; $user = $this->username ?? ''; $pass = $this->password !== null ? ':' . $this->password : ''; $pass = ($user !== '' || $pass !== '') ? $pass . '@' : ''; return $scheme . $user . $pass . $host . $port . $this->getRelativeUrl($includeParams); } /** * Specify data which should be serialized to JSON * @link http://php.net/manual/en/jsonserializable.jsonserialize.php * @return string data which can be serialized by <b>json_encode</b>, * which is a value of any type other than a resource. * @since 5.4.0 */ public function jsonSerialize(): string { return $this->getRelativeUrl(); } public function __toString(): string { return $this->getRelativeUrl(); } } Pecee/Http/Response.php 0000644 00000006453 15025064632 0011026 0 ustar 00 <?php namespace Pecee\Http; use JsonSerializable; use Pecee\Exceptions\InvalidArgumentException; class Response { protected $request; public function __construct(Request $request) { $this->request = $request; } /** * Set the http status code * * @param int $code * @return static */ public function httpCode(int $code): self { http_response_code($code); return $this; } /** * Redirect the response * * @param string $url * @param ?int $httpCode */ public function redirect(string $url, ?int $httpCode = null): void { if ($httpCode !== null) { $this->httpCode($httpCode); } $this->header('location: ' . $url); exit(0); } public function refresh(): void { $this->redirect($this->request->getUrl()->getOriginalUrl()); } /** * Add http authorisation * @param string $name * @return static */ public function auth(string $name = ''): self { $this->headers([ 'WWW-Authenticate: Basic realm="' . $name . '"', 'HTTP/1.0 401 Unauthorized', ]); return $this; } public function cache(string $eTag, int $lastModifiedTime = 2592000): self { $this->headers([ 'Cache-Control: public', sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $lastModifiedTime)), sprintf('Etag: %s', $eTag), ]); $httpModified = $this->request->getHeader('http-if-modified-since'); $httpIfNoneMatch = $this->request->getHeader('http-if-none-match'); if (($httpIfNoneMatch !== null && $httpIfNoneMatch === $eTag) || ($httpModified !== null && strtotime($httpModified) === $lastModifiedTime)) { $this->header('HTTP/1.1 304 Not Modified'); exit(0); } return $this; } /** * Json encode * @param array|JsonSerializable $value * @param ?int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR. * @param int $dept JSON debt. * @throws InvalidArgumentException */ public function json($value, ?int $options = null, int $dept = 512): void { if (($value instanceof JsonSerializable) === false && is_array($value) === false) { throw new InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.'); } $this->header('Content-Type: application/json; charset=utf-8'); echo json_encode($value, $options, $dept); exit(0); } /** * Add header to response * @param string $value * @return static */ public function header(string $value): self { header($value); return $this; } /** * Add multiple headers to response * @param array $headers * @return static */ public function headers(array $headers): self { foreach ($headers as $header) { $this->header($header); } return $this; } } Pecee/Http/Middleware/BaseCsrfVerifier.php 0000644 00000006524 15025064632 0014470 0 ustar 00 <?php namespace Pecee\Http\Middleware; use Pecee\Http\Middleware\Exceptions\TokenMismatchException; use Pecee\Http\Request; use Pecee\Http\Security\CookieTokenProvider; use Pecee\Http\Security\ITokenProvider; class BaseCsrfVerifier implements IMiddleware { public const POST_KEY = 'csrf_token'; public const HEADER_KEY = 'X-CSRF-TOKEN'; /** * Urls to ignore. You can use * to exclude all sub-urls on a given path. * For example: /admin/* * @var array|null */ protected $except; /** * Urls to include. Can be used to include urls from a certain path. * @var array|null */ protected $include; /** * @var ITokenProvider */ protected $tokenProvider; /** * BaseCsrfVerifier constructor. */ public function __construct() { $this->tokenProvider = new CookieTokenProvider(); } /** * Check if the url matches the urls in the except property * @param Request $request * @return bool */ protected function skip(Request $request): bool { if ($this->except === null || count($this->except) === 0) { return false; } foreach($this->except as $url) { $url = rtrim($url, '/'); if ($url[strlen($url) - 1] === '*') { $url = rtrim($url, '*'); $skip = $request->getUrl()->contains($url); } else { $skip = ($url === rtrim($request->getUrl()->getRelativeUrl(false), '/')); } if ($skip === true) { if(is_array($this->include) === true && count($this->include) > 0) { foreach($this->include as $includeUrl) { $includeUrl = rtrim($includeUrl, '/'); if ($includeUrl[strlen($includeUrl) - 1] === '*') { $includeUrl = rtrim($includeUrl, '*'); $skip = !$request->getUrl()->contains($includeUrl); break; } $skip = !($includeUrl === rtrim($request->getUrl()->getRelativeUrl(false), '/')); } } if($skip === false) { continue; } return true; } } return false; } /** * Handle request * * @param Request $request * @throws TokenMismatchException */ public function handle(Request $request): void { if ($this->skip($request) === false && $request->isPostBack() === true) { $token = $request->getInputHandler()->value( static::POST_KEY, $request->getHeader(static::HEADER_KEY), Request::$requestTypesPost ); if ($this->tokenProvider->validate((string)$token) === false) { throw new TokenMismatchException('Invalid CSRF-token.'); } } // Refresh existing token $this->tokenProvider->refresh(); } public function getTokenProvider(): ITokenProvider { return $this->tokenProvider; } /** * Set token provider * @param ITokenProvider $provider */ public function setTokenProvider(ITokenProvider $provider): void { $this->tokenProvider = $provider; } } Pecee/Http/Middleware/Exceptions/TokenMismatchException.php 0000644 00000000167 15025064632 0020047 0 ustar 00 <?php namespace Pecee\Http\Middleware\Exceptions; use Exception; class TokenMismatchException extends Exception { } Pecee/Http/Middleware/IpRestrictAccess.php 0000644 00000002207 15025064632 0014510 0 ustar 00 <?php namespace Pecee\Http\Middleware; use Pecee\Http\Request; use Pecee\SimpleRouter\Exceptions\HttpException; abstract class IpRestrictAccess implements IMiddleware { protected $ipBlacklist = []; protected $ipWhitelist = []; protected function validate(string $ip): bool { // Accept ip that is in white-list if(in_array($ip, $this->ipWhitelist, true) === true) { return true; } foreach ($this->ipBlacklist as $blackIp) { // Blocks range (8.8.*) if ($blackIp[strlen($blackIp) - 1] === '*' && strpos($ip, trim($blackIp, '*')) === 0) { return false; } // Blocks exact match if ($blackIp === $ip) { return false; } } return true; } /** * @param Request $request * @throws HttpException */ public function handle(Request $request): void { if($this->validate((string)$request->getIp()) === false) { throw new HttpException(sprintf('Restricted ip. Access to %s has been blocked', $request->getIp()), 403); } } } Pecee/Http/Middleware/IMiddleware.php 0000644 00000000277 15025064632 0013471 0 ustar 00 <?php namespace Pecee\Http\Middleware; use Pecee\Http\Request; interface IMiddleware { /** * @param Request $request */ public function handle(Request $request): void; } Pecee/Http/Request.php 0000644 00000031121 15025064632 0010646 0 ustar 00 <?php namespace Pecee\Http; use Pecee\Http\Exceptions\MalformedUrlException; use Pecee\Http\Input\InputHandler; use Pecee\Http\Middleware\BaseCsrfVerifier; use Pecee\SimpleRouter\Route\ILoadableRoute; use Pecee\SimpleRouter\Route\RouteUrl; use Pecee\SimpleRouter\SimpleRouter; class Request { public const REQUEST_TYPE_GET = 'get'; public const REQUEST_TYPE_POST = 'post'; public const REQUEST_TYPE_PUT = 'put'; public const REQUEST_TYPE_PATCH = 'patch'; public const REQUEST_TYPE_OPTIONS = 'options'; public const REQUEST_TYPE_DELETE = 'delete'; public const REQUEST_TYPE_HEAD = 'head'; public const CONTENT_TYPE_JSON = 'application/json'; public const CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; public const CONTENT_TYPE_X_FORM_ENCODED = 'application/x-www-form-urlencoded'; public const FORCE_METHOD_KEY = '_method'; /** * All request-types * @var string[] */ public static $requestTypes = [ self::REQUEST_TYPE_GET, self::REQUEST_TYPE_POST, self::REQUEST_TYPE_PUT, self::REQUEST_TYPE_PATCH, self::REQUEST_TYPE_OPTIONS, self::REQUEST_TYPE_DELETE, self::REQUEST_TYPE_HEAD, ]; /** * Post request-types. * @var string[] */ public static $requestTypesPost = [ self::REQUEST_TYPE_POST, self::REQUEST_TYPE_PUT, self::REQUEST_TYPE_PATCH, self::REQUEST_TYPE_DELETE, ]; /** * Additional data * * @var array */ private $data = []; /** * Server headers * @var array */ protected $headers = []; /** * Request ContentType * @var string */ protected $contentType; /** * Request host * @var string */ protected $host; /** * Current request url * @var Url */ protected $url; /** * Request method * @var string */ protected $method; /** * Input handler * @var InputHandler */ protected $inputHandler; /** * Defines if request has pending rewrite * @var bool */ protected $hasPendingRewrite = false; /** * @var ILoadableRoute|null */ protected $rewriteRoute; /** * Rewrite url * @var string|null */ protected $rewriteUrl; /** * @var array */ protected $loadedRoutes = []; /** * Request constructor. * @throws MalformedUrlException */ public function __construct() { foreach ($_SERVER as $key => $value) { $this->headers[strtolower($key)] = $value; $this->headers[str_replace('_', '-', strtolower($key))] = $value; } $this->setHost($this->getHeader('http-host')); // Check if special IIS header exist, otherwise use default. $this->setUrl(new Url($this->getFirstHeader(['unencoded-url', 'request-uri']))); $this->setContentType((string)$this->getHeader('content-type')); $this->setMethod((string)($_POST[static::FORCE_METHOD_KEY] ?? $this->getHeader('request-method'))); $this->inputHandler = new InputHandler($this); } public function isSecure(): bool { return $this->getHeader('http-x-forwarded-proto') === 'https' || $this->getHeader('https') !== null || (int)$this->getHeader('server-port') === 443; } /** * @return Url */ public function getUrl(): Url { return $this->url; } /** * Copy url object * * @return Url */ public function getUrlCopy(): Url { return clone $this->url; } /** * @return string|null */ public function getHost(): ?string { return $this->host; } /** * @return string|null */ public function getMethod(): ?string { return $this->method; } /** * Get http basic auth user * @return string|null */ public function getUser(): ?string { return $this->getHeader('php-auth-user'); } /** * Get http basic auth password * @return string|null */ public function getPassword(): ?string { return $this->getHeader('php-auth-pw'); } /** * Get the csrf token * @return string|null */ public function getCsrfToken(): ?string { return $this->getHeader(BaseCsrfVerifier::HEADER_KEY); } /** * Get all headers * @return array */ public function getHeaders(): array { return $this->headers; } /** * Get id address * If $safe is false, this function will detect Proxys. But the user can edit this header to whatever he wants! * https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php#comment-25086804 * @param bool $safeMode When enabled, only safe non-spoofable headers will be returned. Note this can cause issues when using proxy. * @return string|null */ public function getIp(bool $safeMode = false): ?string { $headers = ['remote-addr']; if($safeMode === false) { $headers = array_merge($headers, [ 'http-cf-connecting-ip', 'http-client-ip', 'http-x-forwarded-for', ]); } return $this->getFirstHeader($headers); } /** * Get remote address/ip * * @alias static::getIp * @return string|null */ public function getRemoteAddr(): ?string { return $this->getIp(); } /** * Get referer * @return string|null */ public function getReferer(): ?string { return $this->getHeader('http-referer'); } /** * Get user agent * @return string|null */ public function getUserAgent(): ?string { return $this->getHeader('http-user-agent'); } /** * Get header value by name * * @param string $name Name of the header. * @param string|mixed|null $defaultValue Value to be returned if header is not found. * @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found. * * @return string|null */ public function getHeader(string $name, $defaultValue = null, bool $tryParse = true): ?string { $name = strtolower($name); $header = $this->headers[$name] ?? null; if ($tryParse === true && $header === null) { if (strpos($name, 'http-') === 0) { // Trying to find client header variant which was not found, searching for header variant without http- prefix. $header = $this->headers[str_replace('http-', '', $name)] ?? null; } else { // Trying to find server variant which was not found, searching for client variant with http- prefix. $header = $this->headers['http-' . $name] ?? null; } } return $header ?? $defaultValue; } /** * Will try to find first header from list of headers. * * @param array $headers * @param mixed|null $defaultValue * @return mixed|null */ public function getFirstHeader(array $headers, $defaultValue = null) { foreach($headers as $header) { $header = $this->getHeader($header); if($header !== null) { return $header; } } return $defaultValue; } /** * Get request content-type * @return string|null */ public function getContentType(): ?string { return $this->contentType; } /** * Set request content-type * @param string $contentType * @return $this */ protected function setContentType(string $contentType): self { if(strpos($contentType, ';') > 0) { $this->contentType = strtolower(substr($contentType, 0, strpos($contentType, ';'))); } else { $this->contentType = strtolower($contentType); } return $this; } /** * Get input class * @return InputHandler */ public function getInputHandler(): InputHandler { return $this->inputHandler; } /** * Is format accepted * * @param string $format * * @return bool */ public function isFormatAccepted(string $format): bool { return ($this->getHeader('http-accept') !== null && stripos($this->getHeader('http-accept'), $format) !== false); } /** * Returns true if the request is made through Ajax * * @return bool */ public function isAjax(): bool { return (strtolower($this->getHeader('http-x-requested-with')) === 'xmlhttprequest'); } /** * Returns true when request-method is type that could contain data in the page body. * * @return bool */ public function isPostBack(): bool { return in_array($this->getMethod(), static::$requestTypesPost, true); } /** * Get accept formats * @return array */ public function getAcceptFormats(): array { return explode(',', $this->getHeader('http-accept')); } /** * @param Url $url */ public function setUrl(Url $url): void { $this->url = $url; if ($this->url->getHost() === null) { $this->url->setHost((string)$this->getHost()); } if($this->isSecure() === true) { $this->url->setScheme('https'); } } /** * @param string|null $host */ public function setHost(?string $host): void { $this->host = $host; } /** * @param string $method */ public function setMethod(string $method): void { $this->method = strtolower($method); } /** * Set rewrite route * * @param ILoadableRoute $route * @return static */ public function setRewriteRoute(ILoadableRoute $route): self { $this->hasPendingRewrite = true; $this->rewriteRoute = SimpleRouter::addDefaultNamespace($route); return $this; } /** * Get rewrite route * * @return ILoadableRoute|null */ public function getRewriteRoute(): ?ILoadableRoute { return $this->rewriteRoute; } /** * Get rewrite url * * @return string|null */ public function getRewriteUrl(): ?string { return $this->rewriteUrl; } /** * Set rewrite url * * @param string $rewriteUrl * @return static */ public function setRewriteUrl(string $rewriteUrl): self { $this->hasPendingRewrite = true; $this->rewriteUrl = rtrim($rewriteUrl, '/') . '/'; return $this; } /** * Set rewrite callback * @param string|\Closure $callback * @return static */ public function setRewriteCallback($callback): self { $this->hasPendingRewrite = true; return $this->setRewriteRoute(new RouteUrl($this->getUrl()->getPath(), $callback)); } /** * Get loaded route * @return ILoadableRoute|null */ public function getLoadedRoute(): ?ILoadableRoute { return (count($this->loadedRoutes) > 0) ? end($this->loadedRoutes) : null; } /** * Get all loaded routes * * @return array */ public function getLoadedRoutes(): array { return $this->loadedRoutes; } /** * Set loaded routes * * @param array $routes * @return static */ public function setLoadedRoutes(array $routes): self { $this->loadedRoutes = $routes; return $this; } /** * Added loaded route * * @param ILoadableRoute $route * @return static */ public function addLoadedRoute(ILoadableRoute $route): self { $this->loadedRoutes[] = $route; return $this; } /** * Returns true if the request contains a rewrite * * @return bool */ public function hasPendingRewrite(): bool { return $this->hasPendingRewrite; } /** * Defines if the current request contains a rewrite. * * @param bool $boolean * @return Request */ public function setHasPendingRewrite(bool $boolean): self { $this->hasPendingRewrite = $boolean; return $this; } public function __isset($name): bool { return array_key_exists($name, $this->data) === true; } public function __set($name, $value = null) { $this->data[$name] = $value; } public function __get($name) { return $this->data[$name] ?? null; } } Pecee/Http/Input/IInputItem.php 0000644 00000000676 15025064632 0012357 0 ustar 00 <?php namespace Pecee\Http\Input; interface IInputItem { public function getIndex(): string; public function setIndex(string $index): self; public function getName(): ?string; public function setName(string $name): self; /** * @return mixed */ public function getValue(); /** * @param mixed $value */ public function setValue($value): self; public function __toString(): string; } Pecee/Http/Input/InputFile.php 0000644 00000013315 15025064632 0012221 0 ustar 00 <?php namespace Pecee\Http\Input; use Pecee\Exceptions\InvalidArgumentException; class InputFile implements IInputItem { /** * @var string */ public $index; /** * @var string */ public $name; /** * @var string|null */ public $filename; /** * @var int|null */ public $size; /** * @var int|null */ public $type; /** * @var int */ public $errors; /** * @var string|null */ public $tmpName; public function __construct(string $index) { $this->index = $index; $this->errors = 0; // Make the name human friendly, by replace _ with space $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); } /** * Create from array * * @param array $values * @throws InvalidArgumentException * @return static */ public static function createFromArray(array $values): self { if (isset($values['index']) === false) { throw new InvalidArgumentException('Index key is required'); } /* Easy way of ensuring that all indexes-are set and not filling the screen with isset() */ $values += [ 'tmp_name' => null, 'type' => null, 'size' => null, 'name' => null, 'error' => null, ]; return (new static($values['index'])) ->setSize((int)$values['size']) ->setError((int)$values['error']) ->setType($values['type']) ->setTmpName($values['tmp_name']) ->setFilename($values['name']); } /** * @return string */ public function getIndex(): string { return $this->index; } /** * Set input index * @param string $index * @return static */ public function setIndex(string $index): IInputItem { $this->index = $index; return $this; } /** * @return string */ public function getSize(): string { return $this->size; } /** * Set file size * @param int $size * @return static */ public function setSize(int $size): IInputItem { $this->size = $size; return $this; } /** * Get mime-type of file * @return string */ public function getMime(): string { return $this->getType(); } /** * @return string */ public function getType(): string { return $this->type; } /** * Set type * @param string $type * @return static */ public function setType(string $type): IInputItem { $this->type = $type; return $this; } /** * Returns extension without "." * * @return string */ public function getExtension(): string { return pathinfo($this->getFilename(), PATHINFO_EXTENSION); } /** * Get human friendly name * * @return string */ public function getName(): ?string { return $this->name; } /** * Set human friendly name. * Useful for adding validation etc. * * @param string $name * @return static */ public function setName(string $name): IInputItem { $this->name = $name; return $this; } /** * Set filename * * @param string $name * @return static */ public function setFilename(string $name): IInputItem { $this->filename = $name; return $this; } /** * Get filename * * @return string mixed */ public function getFilename(): ?string { return $this->filename; } /** * Move the uploaded temporary file to it's new home * * @param string $destination * @return bool */ public function move(string $destination): bool { return move_uploaded_file($this->tmpName, $destination); } /** * Get file contents * * @return string */ public function getContents(): string { return file_get_contents($this->tmpName); } /** * Return true if an upload error occurred. * * @return bool */ public function hasError(): bool { return ($this->getError() !== 0); } /** * Get upload-error code. * * @return int|null */ public function getError(): ?int { return $this->errors; } /** * Set error * * @param int|null $error * @return static */ public function setError(?int $error): IInputItem { $this->errors = (int)$error; return $this; } /** * @return string */ public function getTmpName(): string { return $this->tmpName; } /** * Set file temp. name * @param string $name * @return static */ public function setTmpName(string $name): IInputItem { $this->tmpName = $name; return $this; } public function __toString(): string { return $this->getTmpName(); } public function getValue(): string { return $this->getFilename(); } /** * @param mixed $value * @return static */ public function setValue($value): IInputItem { $this->filename = $value; return $this; } public function toArray(): array { return [ 'tmp_name' => $this->tmpName, 'type' => $this->type, 'size' => $this->size, 'name' => $this->name, 'error' => $this->errors, 'filename' => $this->filename, ]; } } Pecee/Http/Input/InputHandler.php 0000644 00000026314 15025064632 0012722 0 ustar 00 <?php namespace Pecee\Http\Input; use Pecee\Exceptions\InvalidArgumentException; use Pecee\Http\Request; class InputHandler { /** * @var array */ protected $get = []; /** * @var array */ protected $post = []; /** * @var array */ protected $file = []; /** * @var Request */ protected $request; /** * Original post variables * @var array */ protected $originalPost = []; /** * Original get/params variables * @var array */ protected $originalParams = []; /** * Get original file variables * @var array */ protected $originalFile = []; /** * Input constructor. * @param Request $request */ public function __construct(Request $request) { $this->request = $request; $this->parseInputs(); } /** * Parse input values * */ public function parseInputs(): void { /* Parse get requests */ if (count($_GET) !== 0) { $this->originalParams = $_GET; $this->get = $this->parseInputItem($this->originalParams); } /* Parse post requests */ $this->originalPost = $_POST; if ($this->request->isPostBack() === true) { $contents = file_get_contents('php://input'); // Append any PHP-input json if (strpos(trim($contents), '{') === 0) { $post = json_decode($contents, true); if ($post !== false) { $this->originalPost += $post; } } } if (count($this->originalPost) !== 0) { $this->post = $this->parseInputItem($this->originalPost); } /* Parse get requests */ if (count($_FILES) !== 0) { $this->originalFile = $_FILES; $this->file = $this->parseFiles($this->originalFile); } } /** * @param array $files Array with files to parse * @param string|null $parentKey Key from parent (used when parsing nested array). * @return array */ public function parseFiles(array $files, ?string $parentKey = null): array { $list = []; foreach ($files as $key => $value) { // Parse multi dept file array if(isset($value['name']) === false && is_array($value) === true) { $list[$key] = $this->parseFiles($value, $key); continue; } // Handle array input if (is_array($value['name']) === false) { $values = ['index' => $parentKey ?? $key]; try { $list[$key] = InputFile::createFromArray($values + $value); } catch (InvalidArgumentException $e) { } continue; } $keys = [$key]; $files = $this->rearrangeFile($value['name'], $keys, $value); if (isset($list[$key]) === true) { $list[$key][] = $files; } else { $list[$key] = $files; } } return $list; } /** * Rearrange multi-dimensional file object created by PHP. * * @param array $values * @param array $index * @param array|null $original * @return array */ protected function rearrangeFile(array $values, array &$index, ?array $original): array { $originalIndex = $index[0]; array_shift($index); $output = []; foreach ($values as $key => $value) { if (is_array($original['name'][$key]) === false) { try { $file = InputFile::createFromArray([ 'index' => ($key === '' && $originalIndex !== '') ? $originalIndex : $key, 'name' => $original['name'][$key], 'error' => $original['error'][$key], 'tmp_name' => $original['tmp_name'][$key], 'type' => $original['type'][$key], 'size' => $original['size'][$key], ]); if (isset($output[$key]) === true) { $output[$key][] = $file; continue; } $output[$key] = $file; continue; } catch (InvalidArgumentException $e) { } } $index[] = $key; $files = $this->rearrangeFile($value, $index, $original); if (isset($output[$key]) === true) { $output[$key][] = $files; } else { $output[$key] = $files; } } return $output; } /** * Parse input item from array * * @param array $array * @return array */ protected function parseInputItem(array $array): array { $list = []; foreach ($array as $key => $value) { // Handle array input if (is_array($value) === true) { $value = $this->parseInputItem($value); } $list[$key] = new InputItem($key, $value); } return $list; } /** * Find input object * * @param string $index * @param array ...$methods * @return IInputItem|array|null */ public function find(string $index, ...$methods) { $element = null; if(count($methods) > 0) { $methods = is_array(...$methods) ? array_values(...$methods) : $methods; } if (count($methods) === 0 || in_array(Request::REQUEST_TYPE_GET, $methods, true) === true) { $element = $this->get($index); } if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array(Request::REQUEST_TYPE_POST, $methods, true) === true)) { $element = $this->post($index); } if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array('file', $methods, true) === true)) { $element = $this->file($index); } return $element; } protected function getValueFromArray(array $array): array { $output = []; /* @var $item InputItem */ foreach ($array as $key => $item) { if ($item instanceof IInputItem) { $item = $item->getValue(); } $output[$key] = is_array($item) ? $this->getValueFromArray($item) : $item; } return $output; } /** * Get input element value matching index * * @param string $index * @param string|mixed|null $defaultValue * @param array ...$methods * @return string|array */ public function value(string $index, $defaultValue = null, ...$methods) { $input = $this->find($index, ...$methods); if ($input instanceof IInputItem) { $input = $input->getValue(); } /* Handle collection */ if (is_array($input) === true) { $output = $this->getValueFromArray($input); return (count($output) === 0) ? $defaultValue : $output; } return ($input === null || (is_string($input) && trim($input) === '')) ? $defaultValue : $input; } /** * Check if a input-item exist. * If an array is as $index parameter the method returns true if all elements exist. * * @param string|array $index * @param array ...$methods * @return bool */ public function exists($index, ...$methods): bool { // Check array if(is_array($index) === true) { foreach($index as $key) { if($this->value($key, null, ...$methods) === null) { return false; } } return true; } return $this->value($index, null, ...$methods) !== null; } /** * Find post-value by index or return default value. * * @param string $index * @param mixed|null $defaultValue * @return InputItem|array|string|null */ public function post(string $index, $defaultValue = null) { return $this->post[$index] ?? $defaultValue; } /** * Find file by index or return default value. * * @param string $index * @param mixed|null $defaultValue * @return InputFile|array|string|null */ public function file(string $index, $defaultValue = null) { return $this->file[$index] ?? $defaultValue; } /** * Find parameter/query-string by index or return default value. * * @param string $index * @param mixed|null $defaultValue * @return InputItem|array|string|null */ public function get(string $index, $defaultValue = null) { return $this->get[$index] ?? $defaultValue; } /** * Get all get/post items * @param array $filter Only take items in filter * @return array */ public function all(array $filter = []): array { $output = $this->originalParams + $this->originalPost + $this->originalFile; $output = (count($filter) > 0) ? array_intersect_key($output, array_flip($filter)) : $output; foreach ($filter as $filterKey) { if (array_key_exists($filterKey, $output) === false) { $output[$filterKey] = null; } } return $output; } /** * Add GET parameter * * @param string $key * @param InputItem $item */ public function addGet(string $key, InputItem $item): void { $this->get[$key] = $item; } /** * Add POST parameter * * @param string $key * @param InputItem $item */ public function addPost(string $key, InputItem $item): void { $this->post[$key] = $item; } /** * Add FILE parameter * * @param string $key * @param InputFile $item */ public function addFile(string $key, InputFile $item): void { $this->file[$key] = $item; } /** * Get original post variables * @return array */ public function getOriginalPost(): array { return $this->originalPost; } /** * Set original post variables * @param array $post * @return static $this */ public function setOriginalPost(array $post): self { $this->originalPost = $post; return $this; } /** * Get original get variables * @return array */ public function getOriginalParams(): array { return $this->originalParams; } /** * Set original get-variables * @param array $params * @return static $this */ public function setOriginalParams(array $params): self { $this->originalParams = $params; return $this; } /** * Get original file variables * @return array */ public function getOriginalFile(): array { return $this->originalFile; } /** * Set original file posts variables * @param array $file * @return static $this */ public function setOriginalFile(array $file): self { $this->originalFile = $file; return $this; } } Pecee/Http/Input/InputItem.php 0000644 00000004156 15025064632 0012243 0 ustar 00 <?php namespace Pecee\Http\Input; use ArrayAccess; use ArrayIterator; use IteratorAggregate; class InputItem implements ArrayAccess, IInputItem, IteratorAggregate { public $index; public $name; public $value; public function __construct(string $index, $value = null) { $this->index = $index; $this->value = $value; // Make the name human friendly, by replace _ with space $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); } /** * @return string */ public function getIndex(): string { return $this->index; } public function setIndex(string $index): IInputItem { $this->index = $index; return $this; } /** * @return string */ public function getName(): ?string { return $this->name; } /** * Set input name * @param string $name * @return static */ public function setName(string $name): IInputItem { $this->name = $name; return $this; } /** * @return mixed */ public function getValue() { return $this->value; } /** * Set input value * @param mixed $value * @return static */ public function setValue($value): IInputItem { $this->value = $value; return $this; } public function offsetExists($offset): bool { return isset($this->value[$offset]); } public function offsetGet($offset) { if ($this->offsetExists($offset) === true) { return $this->value[$offset]; } return null; } public function offsetSet($offset, $value): void { $this->value[$offset] = $value; } public function offsetUnset($offset): void { unset($this->value[$offset]); } public function __toString(): string { $value = $this->getValue(); return (is_array($value) === true) ? json_encode($value) : $value; } public function getIterator(): ArrayIterator { return new ArrayIterator($this->getValue()); } } Pecee/Http/Security/CookieTokenProvider.php 0000644 00000005365 15025064632 0014765 0 ustar 00 <?php namespace Pecee\Http\Security; use Exception; use Pecee\Http\Security\Exceptions\SecurityException; class CookieTokenProvider implements ITokenProvider { public const CSRF_KEY = 'CSRF-TOKEN'; /** * @var string */ protected $token; /** * @var int */ protected $cookieTimeoutMinutes = 120; /** * CookieTokenProvider constructor. * @throws SecurityException */ public function __construct() { $this->token = ($this->hasToken() === true) ? $_COOKIE[static::CSRF_KEY] : null; if ($this->token === null) { $this->token = $this->generateToken(); } } /** * Generate random identifier for CSRF token * * @return string * @throws SecurityException */ public function generateToken(): string { try { return bin2hex(random_bytes(32)); } catch (Exception $e) { throw new SecurityException($e->getMessage(), (int)$e->getCode(), $e->getPrevious()); } } /** * Validate valid CSRF token * * @param string $token * @return bool */ public function validate(string $token): bool { if ($this->getToken() !== null) { return hash_equals($token, $this->getToken()); } return false; } /** * Set csrf token cookie * Overwrite this method to save the token to another storage like session etc. * * @param string $token */ public function setToken(string $token): void { $this->token = $token; setcookie(static::CSRF_KEY, $token, time() + (60 * $this->cookieTimeoutMinutes), '/', ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly')); } /** * Get csrf token * @param string|null $defaultValue * @return string|null */ public function getToken(?string $defaultValue = null): ?string { return $this->token ?? $defaultValue; } /** * Refresh existing token */ public function refresh(): void { if ($this->token !== null) { $this->setToken($this->token); } } /** * Returns whether the csrf token has been defined * @return bool */ public function hasToken(): bool { return isset($_COOKIE[static::CSRF_KEY]); } /** * Get timeout for cookie in minutes * @return int */ public function getCookieTimeoutMinutes(): int { return $this->cookieTimeoutMinutes; } /** * Set cookie timeout in minutes * @param int $minutes */ public function setCookieTimeoutMinutes(int $minutes): void { $this->cookieTimeoutMinutes = $minutes; } } Pecee/Http/Security/ITokenProvider.php 0000644 00000000753 15025064632 0013740 0 ustar 00 <?php namespace Pecee\Http\Security; interface ITokenProvider { /** * Refresh existing token */ public function refresh(): void; /** * Validate valid CSRF token * * @param string $token * @return bool */ public function validate(string $token): bool; /** * Get token token * * @param string|null $defaultValue * @return string|null */ public function getToken(?string $defaultValue = null): ?string; } Pecee/Http/Security/Exceptions/SecurityException.php 0000644 00000000160 15025064632 0016633 0 ustar 00 <?php namespace Pecee\Http\Security\Exceptions; use Exception; class SecurityException extends Exception { } Pecee/Http/Exceptions/MalformedUrlException.php 0000644 00000000153 15025064632 0015610 0 ustar 00 <?php namespace Pecee\Http\Exceptions; use Exception; class MalformedUrlException extends Exception { } Pecee/Exceptions/InvalidArgumentException.php 0000644 00000000151 15025064632 0015367 0 ustar 00 <?php namespace Pecee\Exceptions; class InvalidArgumentException extends \InvalidArgumentException { } POP3.php 0000644 00000027520 15025064760 0006011 0 ustar 00 <?php /** * PHPMailer POP-Before-SMTP Authentication Class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; /** * PHPMailer POP-Before-SMTP Authentication Class. * Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication. * 1) This class does not support APOP authentication. * 2) Opening and closing lots of POP3 connections can be quite slow. If you need * to send a batch of emails then just perform the authentication once at the start, * and then loop through your mail sending script. Providing this process doesn't * take longer than the verification period lasts on your POP3 server, you should be fine. * 3) This is really ancient technology; you should only need to use it to talk to very old systems. * 4) This POP3 class is deliberately lightweight and incomplete, implementing just * enough to do authentication. * If you want a more complete class there are other POP3 classes for PHP available. * * @author Richard Davey (original author) <rich@corephp.co.uk> * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> */ class POP3 { /** * The POP3 PHPMailer Version number. * * @var string */ const VERSION = '6.6.3'; /** * Default POP3 port number. * * @var int */ const DEFAULT_PORT = 110; /** * Default timeout in seconds. * * @var int */ const DEFAULT_TIMEOUT = 30; /** * POP3 class debug output mode. * Debug output level. * Options: * @see POP3::DEBUG_OFF: No output * @see POP3::DEBUG_SERVER: Server messages, connection/server errors * @see POP3::DEBUG_CLIENT: Client and Server messages, connection/server errors * * @var int */ public $do_debug = self::DEBUG_OFF; /** * POP3 mail server hostname. * * @var string */ public $host; /** * POP3 port number. * * @var int */ public $port; /** * POP3 Timeout Value in seconds. * * @var int */ public $tval; /** * POP3 username. * * @var string */ public $username; /** * POP3 password. * * @var string */ public $password; /** * Resource handle for the POP3 connection socket. * * @var resource */ protected $pop_conn; /** * Are we connected? * * @var bool */ protected $connected = false; /** * Error container. * * @var array */ protected $errors = []; /** * Line break constant. */ const LE = "\r\n"; /** * Debug level for no output. * * @var int */ const DEBUG_OFF = 0; /** * Debug level to show server -> client messages * also shows clients connection errors or errors from server * * @var int */ const DEBUG_SERVER = 1; /** * Debug level to show client -> server and server -> client messages. * * @var int */ const DEBUG_CLIENT = 2; /** * Simple static wrapper for all-in-one POP before SMTP. * * @param string $host The hostname to connect to * @param int|bool $port The port number to connect to * @param int|bool $timeout The timeout value * @param string $username * @param string $password * @param int $debug_level * * @return bool */ public static function popBeforeSmtp( $host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0 ) { $pop = new self(); return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level); } /** * Authenticate with a POP3 server. * A connect, login, disconnect sequence * appropriate for POP-before SMTP authorisation. * * @param string $host The hostname to connect to * @param int|bool $port The port number to connect to * @param int|bool $timeout The timeout value * @param string $username * @param string $password * @param int $debug_level * * @return bool */ public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0) { $this->host = $host; //If no port value provided, use default if (false === $port) { $this->port = static::DEFAULT_PORT; } else { $this->port = (int) $port; } //If no timeout value provided, use default if (false === $timeout) { $this->tval = static::DEFAULT_TIMEOUT; } else { $this->tval = (int) $timeout; } $this->do_debug = $debug_level; $this->username = $username; $this->password = $password; //Reset the error log $this->errors = []; //Connect $result = $this->connect($this->host, $this->port, $this->tval); if ($result) { $login_result = $this->login($this->username, $this->password); if ($login_result) { $this->disconnect(); return true; } } //We need to disconnect regardless of whether the login succeeded $this->disconnect(); return false; } /** * Connect to a POP3 server. * * @param string $host * @param int|bool $port * @param int $tval * * @return bool */ public function connect($host, $port = false, $tval = 30) { //Are we already connected? if ($this->connected) { return true; } //On Windows this will raise a PHP Warning error if the hostname doesn't exist. //Rather than suppress it with @fsockopen, capture it cleanly instead set_error_handler([$this, 'catchWarning']); if (false === $port) { $port = static::DEFAULT_PORT; } //Connect to the POP3 server $errno = 0; $errstr = ''; $this->pop_conn = fsockopen( $host, //POP3 Host $port, //Port # $errno, //Error Number $errstr, //Error Message $tval ); //Timeout (seconds) //Restore the error handler restore_error_handler(); //Did we connect? if (false === $this->pop_conn) { //It would appear not... $this->setError( "Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr" ); return false; } //Increase the stream time-out stream_set_timeout($this->pop_conn, $tval, 0); //Get the POP3 server response $pop3_response = $this->getResponse(); //Check for the +OK if ($this->checkResponse($pop3_response)) { //The connection is established and the POP3 server is talking $this->connected = true; return true; } return false; } /** * Log in to the POP3 server. * Does not support APOP (RFC 2828, 4949). * * @param string $username * @param string $password * * @return bool */ public function login($username = '', $password = '') { if (!$this->connected) { $this->setError('Not connected to POP3 server'); return false; } if (empty($username)) { $username = $this->username; } if (empty($password)) { $password = $this->password; } //Send the Username $this->sendString("USER $username" . static::LE); $pop3_response = $this->getResponse(); if ($this->checkResponse($pop3_response)) { //Send the Password $this->sendString("PASS $password" . static::LE); $pop3_response = $this->getResponse(); if ($this->checkResponse($pop3_response)) { return true; } } return false; } /** * Disconnect from the POP3 server. */ public function disconnect() { $this->sendString('QUIT'); // RFC 1939 shows POP3 server sending a +OK response to the QUIT command. // Try to get it. Ignore any failures here. try { $this->getResponse(); } catch (Exception $e) { //Do nothing } //The QUIT command may cause the daemon to exit, which will kill our connection //So ignore errors here try { @fclose($this->pop_conn); } catch (Exception $e) { //Do nothing } // Clean up attributes. $this->connected = false; $this->pop_conn = false; } /** * Get a response from the POP3 server. * * @param int $size The maximum number of bytes to retrieve * * @return string */ protected function getResponse($size = 128) { $response = fgets($this->pop_conn, $size); if ($this->do_debug >= self::DEBUG_SERVER) { echo 'Server -> Client: ', $response; } return $response; } /** * Send raw data to the POP3 server. * * @param string $string * * @return int */ protected function sendString($string) { if ($this->pop_conn) { if ($this->do_debug >= self::DEBUG_CLIENT) { //Show client messages when debug >= 2 echo 'Client -> Server: ', $string; } return fwrite($this->pop_conn, $string, strlen($string)); } return 0; } /** * Checks the POP3 server response. * Looks for for +OK or -ERR. * * @param string $string * * @return bool */ protected function checkResponse($string) { if (strpos($string, '+OK') !== 0) { $this->setError("Server reported an error: $string"); return false; } return true; } /** * Add an error to the internal error store. * Also display debug output if it's enabled. * * @param string $error */ protected function setError($error) { $this->errors[] = $error; if ($this->do_debug >= self::DEBUG_SERVER) { echo '<pre>'; foreach ($this->errors as $e) { print_r($e); } echo '</pre>'; } } /** * Get an array of error messages, if any. * * @return array */ public function getErrors() { return $this->errors; } /** * POP3 connection error handler. * * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline */ protected function catchWarning($errno, $errstr, $errfile, $errline) { $this->setError( 'Connecting to the POP3 server raised a PHP warning:' . "errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline" ); } } OAuthTokenProvider.php 0000644 00000002762 15025064760 0011025 0 ustar 00 <?php /** * PHPMailer - PHP email creation and transport class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; /** * OAuthTokenProvider - OAuth2 token provider interface. * Provides base64 encoded OAuth2 auth strings for SMTP authentication. * * @see OAuth * @see SMTP::authenticate() * * @author Peter Scopes (pdscopes) * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> */ interface OAuthTokenProvider { /** * Generate a base64-encoded OAuth token ensuring that the access token has not expired. * The string to be base 64 encoded should be in the form: * "user=<user_email_address>\001auth=Bearer <access_token>\001\001" * * @return string */ public function getOauth64(); } OAuth.php 0000644 00000007276 15025064760 0006316 0 ustar 00 <?php /** * PHPMailer - PHP email creation and transport class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; use League\OAuth2\Client\Grant\RefreshToken; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Token\AccessToken; /** * OAuth - OAuth2 authentication wrapper class. * Uses the oauth2-client package from the League of Extraordinary Packages. * * @see http://oauth2-client.thephpleague.com * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> */ class OAuth implements OAuthTokenProvider { /** * An instance of the League OAuth Client Provider. * * @var AbstractProvider */ protected $provider; /** * The current OAuth access token. * * @var AccessToken */ protected $oauthToken; /** * The user's email address, usually used as the login ID * and also the from address when sending email. * * @var string */ protected $oauthUserEmail = ''; /** * The client secret, generated in the app definition of the service you're connecting to. * * @var string */ protected $oauthClientSecret = ''; /** * The client ID, generated in the app definition of the service you're connecting to. * * @var string */ protected $oauthClientId = ''; /** * The refresh token, used to obtain new AccessTokens. * * @var string */ protected $oauthRefreshToken = ''; /** * OAuth constructor. * * @param array $options Associative array containing * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements */ public function __construct($options) { $this->provider = $options['provider']; $this->oauthUserEmail = $options['userName']; $this->oauthClientSecret = $options['clientSecret']; $this->oauthClientId = $options['clientId']; $this->oauthRefreshToken = $options['refreshToken']; } /** * Get a new RefreshToken. * * @return RefreshToken */ protected function getGrant() { return new RefreshToken(); } /** * Get a new AccessToken. * * @return AccessToken */ protected function getToken() { return $this->provider->getAccessToken( $this->getGrant(), ['refresh_token' => $this->oauthRefreshToken] ); } /** * Generate a base64-encoded OAuth token. * * @return string */ public function getOauth64() { //Get a new token if it's not available or has expired if (null === $this->oauthToken || $this->oauthToken->hasExpired()) { $this->oauthToken = $this->getToken(); } return base64_encode( 'user=' . $this->oauthUserEmail . "\001auth=Bearer " . $this->oauthToken . "\001\001" ); } } SMTP.php 0000644 00000134513 15025064760 0006054 0 ustar 00 <?php /** * PHPMailer RFC821 SMTP email transport class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; /** * PHPMailer RFC821 SMTP email transport class. * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. * * @author Chris Ryan * @author Marcus Bointon <phpmailer@synchromedia.co.uk> */ class SMTP { /** * The PHPMailer SMTP version number. * * @var string */ const VERSION = '6.6.3'; /** * SMTP line break constant. * * @var string */ const LE = "\r\n"; /** * The SMTP port to use if one is not specified. * * @var int */ const DEFAULT_PORT = 25; /** * The maximum line length allowed by RFC 5321 section 4.5.3.1.6, * *excluding* a trailing CRLF break. * * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6 * * @var int */ const MAX_LINE_LENGTH = 998; /** * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5, * *including* a trailing CRLF line break. * * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5 * * @var int */ const MAX_REPLY_LENGTH = 512; /** * Debug level for no output. * * @var int */ const DEBUG_OFF = 0; /** * Debug level to show client -> server messages. * * @var int */ const DEBUG_CLIENT = 1; /** * Debug level to show client -> server and server -> client messages. * * @var int */ const DEBUG_SERVER = 2; /** * Debug level to show connection status, client -> server and server -> client messages. * * @var int */ const DEBUG_CONNECTION = 3; /** * Debug level to show all messages. * * @var int */ const DEBUG_LOWLEVEL = 4; /** * Debug output level. * Options: * * self::DEBUG_OFF (`0`) No debug output, default * * self::DEBUG_CLIENT (`1`) Client commands * * self::DEBUG_SERVER (`2`) Client commands and server responses * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages. * * @var int */ public $do_debug = self::DEBUG_OFF; /** * How to handle debug output. * Options: * * `echo` Output plain-text as-is, appropriate for CLI * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output * * `error_log` Output to error log as configured in php.ini * Alternatively, you can provide a callable expecting two params: a message string and the debug level: * * ```php * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; * ``` * * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` * level output is used: * * ```php * $mail->Debugoutput = new myPsr3Logger; * ``` * * @var string|callable|\Psr\Log\LoggerInterface */ public $Debugoutput = 'echo'; /** * Whether to use VERP. * * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path * @see http://www.postfix.org/VERP_README.html Info on VERP * * @var bool */ public $do_verp = false; /** * The timeout value for connection, in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. * * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 * * @var int */ public $Timeout = 300; /** * How long to wait for commands to complete, in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. * * @var int */ public $Timelimit = 300; /** * Patterns to extract an SMTP transaction id from reply to a DATA command. * The first capture group in each regex will be used as the ID. * MS ESMTP returns the message ID, which may not be correct for internal tracking. * * @var string[] */ protected $smtp_transaction_id_patterns = [ 'exim' => '/[\d]{3} OK id=(.*)/', 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', 'Amazon_SES' => '/[\d]{3} Ok (.*)/', 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 'Mailjet' => '/[\d]{3} OK queued as (.*)/', ]; /** * The last transaction ID issued in response to a DATA command, * if one was detected. * * @var string|bool|null */ protected $last_smtp_transaction_id; /** * The socket for the server connection. * * @var ?resource */ protected $smtp_conn; /** * Error information, if any, for the last SMTP command. * * @var array */ protected $error = [ 'error' => '', 'detail' => '', 'smtp_code' => '', 'smtp_code_ex' => '', ]; /** * The reply the server sent to us for HELO. * If null, no HELO string has yet been received. * * @var string|null */ protected $helo_rply; /** * The set of SMTP extensions sent in reply to EHLO command. * Indexes of the array are extension names. * Value at index 'HELO' or 'EHLO' (according to command that was sent) * represents the server name. In case of HELO it is the only element of the array. * Other values can be boolean TRUE or an array containing extension options. * If null, no HELO/EHLO string has yet been received. * * @var array|null */ protected $server_caps; /** * The most recent reply received from the server. * * @var string */ protected $last_reply = ''; /** * Output debugging info via a user-selected method. * * @param string $str Debug string to output * @param int $level The debug level of this message; see DEBUG_* constants * * @see SMTP::$Debugoutput * @see SMTP::$do_debug */ protected function edebug($str, $level = 0) { if ($level > $this->do_debug) { return; } //Is this a PSR-3 logger? if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { $this->Debugoutput->debug($str); return; } //Avoid clash with built-in function names if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { call_user_func($this->Debugoutput, $str, $level); return; } switch ($this->Debugoutput) { case 'error_log': //Don't output, just log error_log($str); break; case 'html': //Cleans up output a bit for a better looking, HTML-safe output echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8' ), "<br>\n"; break; case 'echo': default: //Normalize line breaks $str = preg_replace('/\r\n|\r/m', "\n", $str); echo gmdate('Y-m-d H:i:s'), "\t", //Trim trailing space trim( //Indent for readability, except for trailing break str_replace( "\n", "\n \t ", trim($str) ) ), "\n"; } } /** * Connect to an SMTP server. * * @param string $host SMTP server IP or host name * @param int $port The port number to connect to * @param int $timeout How long to wait for the connection to open * @param array $options An array of options for stream_context_create() * * @return bool */ public function connect($host, $port = null, $timeout = 30, $options = []) { //Clear errors to avoid confusion $this->setError(''); //Make sure we are __not__ connected if ($this->connected()) { //Already connected, generate error $this->setError('Already connected to a server'); return false; } if (empty($port)) { $port = self::DEFAULT_PORT; } //Connect to the SMTP server $this->edebug( "Connection: opening to $host:$port, timeout=$timeout, options=" . (count($options) > 0 ? var_export($options, true) : 'array()'), self::DEBUG_CONNECTION ); $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options); if ($this->smtp_conn === false) { //Error info already set inside `getSMTPConnection()` return false; } $this->edebug('Connection: opened', self::DEBUG_CONNECTION); //Get any announcement $this->last_reply = $this->get_lines(); $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); $responseCode = (int)substr($this->last_reply, 0, 3); if ($responseCode === 220) { return true; } //Anything other than a 220 response means something went wrong //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error //https://tools.ietf.org/html/rfc5321#section-3.1 if ($responseCode === 554) { $this->quit(); } //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down) $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION); $this->close(); return false; } /** * Create connection to the SMTP server. * * @param string $host SMTP server IP or host name * @param int $port The port number to connect to * @param int $timeout How long to wait for the connection to open * @param array $options An array of options for stream_context_create() * * @return false|resource */ protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = []) { static $streamok; //This is enabled by default since 5.0.0 but some providers disable it //Check this once and cache the result if (null === $streamok) { $streamok = function_exists('stream_socket_client'); } $errno = 0; $errstr = ''; if ($streamok) { $socket_context = stream_context_create($options); set_error_handler([$this, 'errorHandler']); $connection = stream_socket_client( $host . ':' . $port, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $socket_context ); } else { //Fall back to fsockopen which should work in more places, but is missing some features $this->edebug( 'Connection: stream_socket_client not available, falling back to fsockopen', self::DEBUG_CONNECTION ); set_error_handler([$this, 'errorHandler']); $connection = fsockopen( $host, $port, $errno, $errstr, $timeout ); } restore_error_handler(); //Verify we connected properly if (!is_resource($connection)) { $this->setError( 'Failed to connect to server', '', (string) $errno, $errstr ); $this->edebug( 'SMTP ERROR: ' . $this->error['error'] . ": $errstr ($errno)", self::DEBUG_CLIENT ); return false; } //SMTP server can take longer to respond, give longer timeout for first read //Windows does not have support for this timeout function if (strpos(PHP_OS, 'WIN') !== 0) { $max = (int)ini_get('max_execution_time'); //Don't bother if unlimited, or if set_time_limit is disabled if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) { @set_time_limit($timeout); } stream_set_timeout($connection, $timeout, 0); } return $connection; } /** * Initiate a TLS (encrypted) session. * * @return bool */ public function startTLS() { if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { return false; } //Allow the best TLS version(s) we can $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT //so add them back in manually if we can if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; } //Begin encrypted connection set_error_handler([$this, 'errorHandler']); $crypto_ok = stream_socket_enable_crypto( $this->smtp_conn, true, $crypto_method ); restore_error_handler(); return (bool) $crypto_ok; } /** * Perform SMTP authentication. * Must be run after hello(). * * @see hello() * * @param string $username The user name * @param string $password The password * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication * * @return bool True if successfully authenticated */ public function authenticate( $username, $password, $authtype = null, $OAuth = null ) { if (!$this->server_caps) { $this->setError('Authentication is not allowed before HELO/EHLO'); return false; } if (array_key_exists('EHLO', $this->server_caps)) { //SMTP extensions are available; try to find a proper authentication method if (!array_key_exists('AUTH', $this->server_caps)) { $this->setError('Authentication is not allowed at this stage'); //'at this stage' means that auth may be allowed after the stage changes //e.g. after STARTTLS return false; } $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); $this->edebug( 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), self::DEBUG_LOWLEVEL ); //If we have requested a specific auth type, check the server supports it before trying others if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) { $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); $authtype = null; } if (empty($authtype)) { //If no auth mechanism is specified, attempt to use these, in this order //Try CRAM-MD5 first as it's more secure than the others foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { if (in_array($method, $this->server_caps['AUTH'], true)) { $authtype = $method; break; } } if (empty($authtype)) { $this->setError('No supported authentication methods found'); return false; } $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); } if (!in_array($authtype, $this->server_caps['AUTH'], true)) { $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); return false; } } elseif (empty($authtype)) { $authtype = 'LOGIN'; } switch ($authtype) { case 'PLAIN': //Start authentication if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { return false; } //Send encoded username and password if ( //Format from https://tools.ietf.org/html/rfc4616#section-2 //We skip the first field (it's forgery), so the string starts with a null byte !$this->sendCommand( 'User & Password', base64_encode("\0" . $username . "\0" . $password), 235 ) ) { return false; } break; case 'LOGIN': //Start authentication if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { return false; } if (!$this->sendCommand('Username', base64_encode($username), 334)) { return false; } if (!$this->sendCommand('Password', base64_encode($password), 235)) { return false; } break; case 'CRAM-MD5': //Start authentication if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { return false; } //Get the challenge $challenge = base64_decode(substr($this->last_reply, 4)); //Build the response $response = $username . ' ' . $this->hmac($challenge, $password); //send encoded credentials return $this->sendCommand('Username', base64_encode($response), 235); case 'XOAUTH2': //The OAuth instance must be set up prior to requesting auth. if (null === $OAuth) { return false; } $oauth = $OAuth->getOauth64(); //Start authentication if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { return false; } break; default: $this->setError("Authentication method \"$authtype\" is not supported"); return false; } return true; } /** * Calculate an MD5 HMAC hash. * Works like hash_hmac('md5', $data, $key) * in case that function is not available. * * @param string $data The data to hash * @param string $key The key to hash with * * @return string */ protected function hmac($data, $key) { if (function_exists('hash_hmac')) { return hash_hmac('md5', $data, $key); } //The following borrowed from //http://php.net/manual/en/function.mhash.php#27225 //RFC 2104 HMAC implementation for php. //Creates an md5 HMAC. //Eliminates the need to install mhash to compute a HMAC //by Lance Rushing $bytelen = 64; //byte length for md5 if (strlen($key) > $bytelen) { $key = pack('H*', md5($key)); } $key = str_pad($key, $bytelen, chr(0x00)); $ipad = str_pad('', $bytelen, chr(0x36)); $opad = str_pad('', $bytelen, chr(0x5c)); $k_ipad = $key ^ $ipad; $k_opad = $key ^ $opad; return md5($k_opad . pack('H*', md5($k_ipad . $data))); } /** * Check connection state. * * @return bool True if connected */ public function connected() { if (is_resource($this->smtp_conn)) { $sock_status = stream_get_meta_data($this->smtp_conn); if ($sock_status['eof']) { //The socket is valid but we are not connected $this->edebug( 'SMTP NOTICE: EOF caught while checking if connected', self::DEBUG_CLIENT ); $this->close(); return false; } return true; //everything looks good } return false; } /** * Close the socket and clean up the state of the class. * Don't use this function without first trying to use QUIT. * * @see quit() */ public function close() { $this->setError(''); $this->server_caps = null; $this->helo_rply = null; if (is_resource($this->smtp_conn)) { //Close the connection and cleanup fclose($this->smtp_conn); $this->smtp_conn = null; //Makes for cleaner serialization $this->edebug('Connection: closed', self::DEBUG_CONNECTION); } } /** * Send an SMTP DATA command. * Issues a data command and sends the msg_data to the server, * finalizing the mail transaction. $msg_data is the message * that is to be send with the headers. Each header needs to be * on a single line followed by a <CRLF> with the message headers * and the message body being separated by an additional <CRLF>. * Implements RFC 821: DATA <CRLF>. * * @param string $msg_data Message data to send * * @return bool */ public function data($msg_data) { //This will use the standard timelimit if (!$this->sendCommand('DATA', 'DATA', 354)) { return false; } /* The server is ready to accept data! * According to rfc821 we should not send more than 1000 characters on a single line (including the LE) * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into * smaller lines to fit within the limit. * We will also look for lines that start with a '.' and prepend an additional '.'. * NOTE: this does not count towards line-length limit. */ //Normalize line breaks before exploding $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data)); /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field * of the first line (':' separated) does not contain a space then it _should_ be a header and we will * process all lines before a blank line as headers. */ $field = substr($lines[0], 0, strpos($lines[0], ':')); $in_headers = false; if (!empty($field) && strpos($field, ' ') === false) { $in_headers = true; } foreach ($lines as $line) { $lines_out = []; if ($in_headers && $line === '') { $in_headers = false; } //Break this line up into several smaller lines if it's too long //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), while (isset($line[self::MAX_LINE_LENGTH])) { //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on //so as to avoid breaking in the middle of a word $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); //Deliberately matches both false and 0 if (!$pos) { //No nice break found, add a hard break $pos = self::MAX_LINE_LENGTH - 1; $lines_out[] = substr($line, 0, $pos); $line = substr($line, $pos); } else { //Break at the found point $lines_out[] = substr($line, 0, $pos); //Move along by the amount we dealt with $line = substr($line, $pos + 1); } //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 if ($in_headers) { $line = "\t" . $line; } } $lines_out[] = $line; //Send the lines to the server foreach ($lines_out as $line_out) { //Dot-stuffing as per RFC5321 section 4.5.2 //https://tools.ietf.org/html/rfc5321#section-4.5.2 if (!empty($line_out) && $line_out[0] === '.') { $line_out = '.' . $line_out; } $this->client_send($line_out . static::LE, 'DATA'); } } //Message data has been sent, complete the command //Increase timelimit for end of DATA command $savetimelimit = $this->Timelimit; $this->Timelimit *= 2; $result = $this->sendCommand('DATA END', '.', 250); $this->recordLastTransactionID(); //Restore timelimit $this->Timelimit = $savetimelimit; return $result; } /** * Send an SMTP HELO or EHLO command. * Used to identify the sending server to the receiving server. * This makes sure that client and server are in a known state. * Implements RFC 821: HELO <SP> <domain> <CRLF> * and RFC 2821 EHLO. * * @param string $host The host name or IP to connect to * * @return bool */ public function hello($host = '') { //Try extended hello first (RFC 2821) if ($this->sendHello('EHLO', $host)) { return true; } //Some servers shut down the SMTP service here (RFC 5321) if (substr($this->helo_rply, 0, 3) == '421') { return false; } return $this->sendHello('HELO', $host); } /** * Send an SMTP HELO or EHLO command. * Low-level implementation used by hello(). * * @param string $hello The HELO string * @param string $host The hostname to say we are * * @return bool * * @see hello() */ protected function sendHello($hello, $host) { $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); $this->helo_rply = $this->last_reply; if ($noerror) { $this->parseHelloFields($hello); } else { $this->server_caps = null; } return $noerror; } /** * Parse a reply to HELO/EHLO command to discover server extensions. * In case of HELO, the only parameter that can be discovered is a server name. * * @param string $type `HELO` or `EHLO` */ protected function parseHelloFields($type) { $this->server_caps = []; $lines = explode("\n", $this->helo_rply); foreach ($lines as $n => $s) { //First 4 chars contain response code followed by - or space $s = trim(substr($s, 4)); if (empty($s)) { continue; } $fields = explode(' ', $s); if (!empty($fields)) { if (!$n) { $name = $type; $fields = $fields[0]; } else { $name = array_shift($fields); switch ($name) { case 'SIZE': $fields = ($fields ? $fields[0] : 0); break; case 'AUTH': if (!is_array($fields)) { $fields = []; } break; default: $fields = true; } } $this->server_caps[$name] = $fields; } } } /** * Send an SMTP MAIL command. * Starts a mail transaction from the email address specified in * $from. Returns true if successful or false otherwise. If True * the mail transaction is started and then one or more recipient * commands may be called followed by a data command. * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>. * * @param string $from Source address of this message * * @return bool */ public function mail($from) { $useVerp = ($this->do_verp ? ' XVERP' : ''); return $this->sendCommand( 'MAIL FROM', 'MAIL FROM:<' . $from . '>' . $useVerp, 250 ); } /** * Send an SMTP QUIT command. * Closes the socket if there is no error or the $close_on_error argument is true. * Implements from RFC 821: QUIT <CRLF>. * * @param bool $close_on_error Should the connection close if an error occurs? * * @return bool */ public function quit($close_on_error = true) { $noerror = $this->sendCommand('QUIT', 'QUIT', 221); $err = $this->error; //Save any error if ($noerror || $close_on_error) { $this->close(); $this->error = $err; //Restore any error from the quit command } return $noerror; } /** * Send an SMTP RCPT command. * Sets the TO argument to $toaddr. * Returns true if the recipient was accepted false if it was rejected. * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>. * * @param string $address The address the message is being sent to * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE * or DELAY. If you specify NEVER all other notifications are ignored. * * @return bool */ public function recipient($address, $dsn = '') { if (empty($dsn)) { $rcpt = 'RCPT TO:<' . $address . '>'; } else { $dsn = strtoupper($dsn); $notify = []; if (strpos($dsn, 'NEVER') !== false) { $notify[] = 'NEVER'; } else { foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) { if (strpos($dsn, $value) !== false) { $notify[] = $value; } } } $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify); } return $this->sendCommand( 'RCPT TO', $rcpt, [250, 251] ); } /** * Send an SMTP RSET command. * Abort any transaction that is currently in progress. * Implements RFC 821: RSET <CRLF>. * * @return bool True on success */ public function reset() { return $this->sendCommand('RSET', 'RSET', 250); } /** * Send a command to an SMTP server and check its return code. * * @param string $command The command name - not sent to the server * @param string $commandstring The actual command to send * @param int|array $expect One or more expected integer success codes * * @return bool True on success */ protected function sendCommand($command, $commandstring, $expect) { if (!$this->connected()) { $this->setError("Called $command without being connected"); return false; } //Reject line breaks in all commands if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) { $this->setError("Command '$command' contained line breaks"); return false; } $this->client_send($commandstring . static::LE, $command); $this->last_reply = $this->get_lines(); //Fetch SMTP code and possible error code explanation $matches = []; if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) { $code = (int) $matches[1]; $code_ex = (count($matches) > 2 ? $matches[2] : null); //Cut off error code from each response line $detail = preg_replace( "/{$code}[ -]" . ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', '', $this->last_reply ); } else { //Fall back to simple parsing if regex fails $code = (int) substr($this->last_reply, 0, 3); $code_ex = null; $detail = substr($this->last_reply, 4); } $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); if (!in_array($code, (array) $expect, true)) { $this->setError( "$command command failed", $detail, $code, $code_ex ); $this->edebug( 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, self::DEBUG_CLIENT ); return false; } //Don't clear the error store when using keepalive if ($command !== 'RSET') { $this->setError(''); } return true; } /** * Send an SMTP SAML command. * Starts a mail transaction from the email address specified in $from. * Returns true if successful or false otherwise. If True * the mail transaction is started and then one or more recipient * commands may be called followed by a data command. This command * will send the message to the users terminal if they are logged * in and send them an email. * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>. * * @param string $from The address the message is from * * @return bool */ public function sendAndMail($from) { return $this->sendCommand('SAML', "SAML FROM:$from", 250); } /** * Send an SMTP VRFY command. * * @param string $name The name to verify * * @return bool */ public function verify($name) { return $this->sendCommand('VRFY', "VRFY $name", [250, 251]); } /** * Send an SMTP NOOP command. * Used to keep keep-alives alive, doesn't actually do anything. * * @return bool */ public function noop() { return $this->sendCommand('NOOP', 'NOOP', 250); } /** * Send an SMTP TURN command. * This is an optional command for SMTP that this class does not support. * This method is here to make the RFC821 Definition complete for this class * and _may_ be implemented in future. * Implements from RFC 821: TURN <CRLF>. * * @return bool */ public function turn() { $this->setError('The SMTP TURN command is not implemented'); $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); return false; } /** * Send raw data to the server. * * @param string $data The data to send * @param string $command Optionally, the command this is part of, used only for controlling debug output * * @return int|bool The number of bytes sent to the server or false on error */ public function client_send($data, $command = '') { //If SMTP transcripts are left enabled, or debug output is posted online //it can leak credentials, so hide credentials in all but lowest level if ( self::DEBUG_LOWLEVEL > $this->do_debug && in_array($command, ['User & Password', 'Username', 'Password'], true) ) { $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT); } else { $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT); } set_error_handler([$this, 'errorHandler']); $result = fwrite($this->smtp_conn, $data); restore_error_handler(); return $result; } /** * Get the latest error. * * @return array */ public function getError() { return $this->error; } /** * Get SMTP extensions available on the server. * * @return array|null */ public function getServerExtList() { return $this->server_caps; } /** * Get metadata about the SMTP server from its HELO/EHLO response. * The method works in three ways, dependent on argument value and current state: * 1. HELO/EHLO has not been sent - returns null and populates $this->error. * 2. HELO has been sent - * $name == 'HELO': returns server name * $name == 'EHLO': returns boolean false * $name == any other string: returns null and populates $this->error * 3. EHLO has been sent - * $name == 'HELO'|'EHLO': returns the server name * $name == any other string: if extension $name exists, returns True * or its options (e.g. AUTH mechanisms supported). Otherwise returns False. * * @param string $name Name of SMTP extension or 'HELO'|'EHLO' * * @return string|bool|null */ public function getServerExt($name) { if (!$this->server_caps) { $this->setError('No HELO/EHLO was sent'); return null; } if (!array_key_exists($name, $this->server_caps)) { if ('HELO' === $name) { return $this->server_caps['EHLO']; } if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) { return false; } $this->setError('HELO handshake was used; No information about server extensions available'); return null; } return $this->server_caps[$name]; } /** * Get the last reply from the server. * * @return string */ public function getLastReply() { return $this->last_reply; } /** * Read the SMTP server's response. * Either before eof or socket timeout occurs on the operation. * With SMTP we can tell if we have more lines to read if the * 4th character is '-' symbol. If it is a space then we don't * need to read anything else. * * @return string */ protected function get_lines() { //If the connection is bad, give up straight away if (!is_resource($this->smtp_conn)) { return ''; } $data = ''; $endtime = 0; stream_set_timeout($this->smtp_conn, $this->Timeout); if ($this->Timelimit > 0) { $endtime = time() + $this->Timelimit; } $selR = [$this->smtp_conn]; $selW = null; while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { //Must pass vars in here as params are by reference //solution for signals inspired by https://github.com/symfony/symfony/pull/6540 set_error_handler([$this, 'errorHandler']); $n = stream_select($selR, $selW, $selW, $this->Timelimit); restore_error_handler(); if ($n === false) { $message = $this->getError()['detail']; $this->edebug( 'SMTP -> get_lines(): select failed (' . $message . ')', self::DEBUG_LOWLEVEL ); //stream_select returns false when the `select` system call is interrupted //by an incoming signal, try the select again if (stripos($message, 'interrupted system call') !== false) { $this->edebug( 'SMTP -> get_lines(): retrying stream_select', self::DEBUG_LOWLEVEL ); $this->setError(''); continue; } break; } if (!$n) { $this->edebug( 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)', self::DEBUG_LOWLEVEL ); break; } //Deliberate noise suppression - errors are handled afterwards $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH); $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL); $data .= $str; //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled), //or 4th character is a space or a line break char, we are done reading, break the loop. //String array access is a significant micro-optimisation over strlen if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") { break; } //Timed-out? Log and break $info = stream_get_meta_data($this->smtp_conn); if ($info['timed_out']) { $this->edebug( 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)', self::DEBUG_LOWLEVEL ); break; } //Now check if reads took too long if ($endtime && time() > $endtime) { $this->edebug( 'SMTP -> get_lines(): timelimit reached (' . $this->Timelimit . ' sec)', self::DEBUG_LOWLEVEL ); break; } } return $data; } /** * Enable or disable VERP address generation. * * @param bool $enabled */ public function setVerp($enabled = false) { $this->do_verp = $enabled; } /** * Get VERP address generation mode. * * @return bool */ public function getVerp() { return $this->do_verp; } /** * Set error messages and codes. * * @param string $message The error message * @param string $detail Further detail on the error * @param string $smtp_code An associated SMTP error code * @param string $smtp_code_ex Extended SMTP code */ protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '') { $this->error = [ 'error' => $message, 'detail' => $detail, 'smtp_code' => $smtp_code, 'smtp_code_ex' => $smtp_code_ex, ]; } /** * Set debug output method. * * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it */ public function setDebugOutput($method = 'echo') { $this->Debugoutput = $method; } /** * Get debug output method. * * @return string */ public function getDebugOutput() { return $this->Debugoutput; } /** * Set debug output level. * * @param int $level */ public function setDebugLevel($level = 0) { $this->do_debug = $level; } /** * Get debug output level. * * @return int */ public function getDebugLevel() { return $this->do_debug; } /** * Set SMTP timeout. * * @param int $timeout The timeout duration in seconds */ public function setTimeout($timeout = 0) { $this->Timeout = $timeout; } /** * Get SMTP timeout. * * @return int */ public function getTimeout() { return $this->Timeout; } /** * Reports an error number and string. * * @param int $errno The error number returned by PHP * @param string $errmsg The error message returned by PHP * @param string $errfile The file the error occurred in * @param int $errline The line number the error occurred on */ protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0) { $notice = 'Connection failed.'; $this->setError( $notice, $errmsg, (string) $errno ); $this->edebug( "$notice Error #$errno: $errmsg [$errfile line $errline]", self::DEBUG_CONNECTION ); } /** * Extract and return the ID of the last SMTP transaction based on * a list of patterns provided in SMTP::$smtp_transaction_id_patterns. * Relies on the host providing the ID in response to a DATA command. * If no reply has been received yet, it will return null. * If no pattern was matched, it will return false. * * @return bool|string|null */ protected function recordLastTransactionID() { $reply = $this->getLastReply(); if (empty($reply)) { $this->last_smtp_transaction_id = null; } else { $this->last_smtp_transaction_id = false; foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { $matches = []; if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) { $this->last_smtp_transaction_id = trim($matches[1]); break; } } } return $this->last_smtp_transaction_id; } /** * Get the queue/transaction ID of the last SMTP transaction * If no reply has been received yet, it will return null. * If no pattern was matched, it will return false. * * @return bool|string|null * * @see recordLastTransactionID() */ public function getLastTransactionID() { return $this->last_smtp_transaction_id; } } PHPMailer.php 0000644 00000533433 15025064760 0007056 0 ustar 00 <?php /** * PHPMailer - PHP email creation and transport class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; /** * PHPMailer - PHP email creation and transport class. * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) */ class PHPMailer { const CHARSET_ASCII = 'us-ascii'; const CHARSET_ISO88591 = 'iso-8859-1'; const CHARSET_UTF8 = 'utf-8'; const CONTENT_TYPE_PLAINTEXT = 'text/plain'; const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; const CONTENT_TYPE_TEXT_HTML = 'text/html'; const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; const ENCODING_7BIT = '7bit'; const ENCODING_8BIT = '8bit'; const ENCODING_BASE64 = 'base64'; const ENCODING_BINARY = 'binary'; const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; const ENCRYPTION_STARTTLS = 'tls'; const ENCRYPTION_SMTPS = 'ssl'; const ICAL_METHOD_REQUEST = 'REQUEST'; const ICAL_METHOD_PUBLISH = 'PUBLISH'; const ICAL_METHOD_REPLY = 'REPLY'; const ICAL_METHOD_ADD = 'ADD'; const ICAL_METHOD_CANCEL = 'CANCEL'; const ICAL_METHOD_REFRESH = 'REFRESH'; const ICAL_METHOD_COUNTER = 'COUNTER'; const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER'; /** * Email priority. * Options: null (default), 1 = High, 3 = Normal, 5 = low. * When null, the header is not set at all. * * @var int|null */ public $Priority; /** * The character set of the message. * * @var string */ public $CharSet = self::CHARSET_ISO88591; /** * The MIME Content-type of the message. * * @var string */ public $ContentType = self::CONTENT_TYPE_PLAINTEXT; /** * The message encoding. * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". * * @var string */ public $Encoding = self::ENCODING_8BIT; /** * Holds the most recent mailer error message. * * @var string */ public $ErrorInfo = ''; /** * The From email address for the message. * * @var string */ public $From = ''; /** * The From name of the message. * * @var string */ public $FromName = ''; /** * The envelope sender of the message. * This will usually be turned into a Return-Path header by the receiver, * and is the address that bounces will be sent to. * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. * * @var string */ public $Sender = ''; /** * The Subject of the message. * * @var string */ public $Subject = ''; /** * An HTML or plain text message body. * If HTML then call isHTML(true). * * @var string */ public $Body = ''; /** * The plain-text message body. * This body can be read by mail clients that do not have HTML email * capability such as mutt & Eudora. * Clients that can read HTML will view the normal Body. * * @var string */ public $AltBody = ''; /** * An iCal message part body. * Only supported in simple alt or alt_inline message types * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. * * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ * @see http://kigkonsult.se/iCalcreator/ * * @var string */ public $Ical = ''; /** * Value-array of "method" in Contenttype header "text/calendar" * * @var string[] */ protected static $IcalMethods = [ self::ICAL_METHOD_REQUEST, self::ICAL_METHOD_PUBLISH, self::ICAL_METHOD_REPLY, self::ICAL_METHOD_ADD, self::ICAL_METHOD_CANCEL, self::ICAL_METHOD_REFRESH, self::ICAL_METHOD_COUNTER, self::ICAL_METHOD_DECLINECOUNTER, ]; /** * The complete compiled MIME message body. * * @var string */ protected $MIMEBody = ''; /** * The complete compiled MIME message headers. * * @var string */ protected $MIMEHeader = ''; /** * Extra headers that createHeader() doesn't fold in. * * @var string */ protected $mailHeader = ''; /** * Word-wrap the message body to this number of chars. * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. * * @see static::STD_LINE_LENGTH * * @var int */ public $WordWrap = 0; /** * Which method to use to send mail. * Options: "mail", "sendmail", or "smtp". * * @var string */ public $Mailer = 'mail'; /** * The path to the sendmail program. * * @var string */ public $Sendmail = '/usr/sbin/sendmail'; /** * Whether mail() uses a fully sendmail-compatible MTA. * One which supports sendmail's "-oi -f" options. * * @var bool */ public $UseSendmailOptions = true; /** * The email address that a reading confirmation should be sent to, also known as read receipt. * * @var string */ public $ConfirmReadingTo = ''; /** * The hostname to use in the Message-ID header and as default HELO string. * If empty, PHPMailer attempts to find one with, in order, * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value * 'localhost.localdomain'. * * @see PHPMailer::$Helo * * @var string */ public $Hostname = ''; /** * An ID to be used in the Message-ID header. * If empty, a unique id will be generated. * You can set your own, but it must be in the format "<id@domain>", * as defined in RFC5322 section 3.6.4 or it will be ignored. * * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 * * @var string */ public $MessageID = ''; /** * The message Date to be used in the Date header. * If empty, the current date will be added. * * @var string */ public $MessageDate = ''; /** * SMTP hosts. * Either a single hostname or multiple semicolon-delimited hostnames. * You can also specify a different port * for each host by using this format: [hostname:port] * (e.g. "smtp1.example.com:25;smtp2.example.com"). * You can also specify encryption type, for example: * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). * Hosts will be tried in order. * * @var string */ public $Host = 'localhost'; /** * The default SMTP server port. * * @var int */ public $Port = 25; /** * The SMTP HELO/EHLO name used for the SMTP connection. * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find * one with the same method described above for $Hostname. * * @see PHPMailer::$Hostname * * @var string */ public $Helo = ''; /** * What kind of encryption to use on the SMTP connection. * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS. * * @var string */ public $SMTPSecure = ''; /** * Whether to enable TLS encryption automatically if a server supports it, * even if `SMTPSecure` is not set to 'tls'. * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. * * @var bool */ public $SMTPAutoTLS = true; /** * Whether to use SMTP authentication. * Uses the Username and Password properties. * * @see PHPMailer::$Username * @see PHPMailer::$Password * * @var bool */ public $SMTPAuth = false; /** * Options array passed to stream_context_create when connecting via SMTP. * * @var array */ public $SMTPOptions = []; /** * SMTP username. * * @var string */ public $Username = ''; /** * SMTP password. * * @var string */ public $Password = ''; /** * SMTP auth type. * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified. * * @var string */ public $AuthType = ''; /** * An implementation of the PHPMailer OAuthTokenProvider interface. * * @var OAuthTokenProvider */ protected $oauth; /** * The SMTP server timeout in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. * * @var int */ public $Timeout = 300; /** * Comma separated list of DSN notifications * 'NEVER' under no circumstances a DSN must be returned to the sender. * If you use NEVER all other notifications will be ignored. * 'SUCCESS' will notify you when your mail has arrived at its destination. * 'FAILURE' will arrive if an error occurred during delivery. * 'DELAY' will notify you if there is an unusual delay in delivery, but the actual * delivery's outcome (success or failure) is not yet decided. * * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY */ public $dsn = ''; /** * SMTP class debug output mode. * Debug output level. * Options: * @see SMTP::DEBUG_OFF: No output * @see SMTP::DEBUG_CLIENT: Client messages * @see SMTP::DEBUG_SERVER: Client and server messages * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed * * @see SMTP::$do_debug * * @var int */ public $SMTPDebug = 0; /** * How to handle debug output. * Options: * * `echo` Output plain-text as-is, appropriate for CLI * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output * * `error_log` Output to error log as configured in php.ini * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. * Alternatively, you can provide a callable expecting two params: a message string and the debug level: * * ```php * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; * ``` * * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` * level output is used: * * ```php * $mail->Debugoutput = new myPsr3Logger; * ``` * * @see SMTP::$Debugoutput * * @var string|callable|\Psr\Log\LoggerInterface */ public $Debugoutput = 'echo'; /** * Whether to keep the SMTP connection open after each message. * If this is set to true then the connection will remain open after a send, * and closing the connection will require an explicit call to smtpClose(). * It's a good idea to use this if you are sending multiple messages as it reduces overhead. * See the mailing list example for how to use it. * * @var bool */ public $SMTPKeepAlive = false; /** * Whether to split multiple to addresses into multiple messages * or send them all in one message. * Only supported in `mail` and `sendmail` transports, not in SMTP. * * @var bool * * @deprecated 6.0.0 PHPMailer isn't a mailing list manager! */ public $SingleTo = false; /** * Storage for addresses when SingleTo is enabled. * * @var array */ protected $SingleToArray = []; /** * Whether to generate VERP addresses on send. * Only applicable when sending via SMTP. * * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path * @see http://www.postfix.org/VERP_README.html Postfix VERP info * * @var bool */ public $do_verp = false; /** * Whether to allow sending messages with an empty body. * * @var bool */ public $AllowEmpty = false; /** * DKIM selector. * * @var string */ public $DKIM_selector = ''; /** * DKIM Identity. * Usually the email address used as the source of the email. * * @var string */ public $DKIM_identity = ''; /** * DKIM passphrase. * Used if your key is encrypted. * * @var string */ public $DKIM_passphrase = ''; /** * DKIM signing domain name. * * @example 'example.com' * * @var string */ public $DKIM_domain = ''; /** * DKIM Copy header field values for diagnostic use. * * @var bool */ public $DKIM_copyHeaderFields = true; /** * DKIM Extra signing headers. * * @example ['List-Unsubscribe', 'List-Help'] * * @var array */ public $DKIM_extraHeaders = []; /** * DKIM private key file path. * * @var string */ public $DKIM_private = ''; /** * DKIM private key string. * * If set, takes precedence over `$DKIM_private`. * * @var string */ public $DKIM_private_string = ''; /** * Callback Action function name. * * The function that handles the result of the send email action. * It is called out by send() for each email sent. * * Value can be any php callable: http://www.php.net/is_callable * * Parameters: * bool $result result of the send action * array $to email addresses of the recipients * array $cc cc email addresses * array $bcc bcc email addresses * string $subject the subject * string $body the email body * string $from email address of sender * string $extra extra information of possible use * "smtp_transaction_id' => last smtp transaction id * * @var string */ public $action_function = ''; /** * What to put in the X-Mailer header. * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use. * * @var string|null */ public $XMailer = ''; /** * Which validator to use by default when validating email addresses. * May be a callable to inject your own validator, but there are several built-in validators. * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. * * @see PHPMailer::validateAddress() * * @var string|callable */ public static $validator = 'php'; /** * An instance of the SMTP sender class. * * @var SMTP */ protected $smtp; /** * The array of 'to' names and addresses. * * @var array */ protected $to = []; /** * The array of 'cc' names and addresses. * * @var array */ protected $cc = []; /** * The array of 'bcc' names and addresses. * * @var array */ protected $bcc = []; /** * The array of reply-to names and addresses. * * @var array */ protected $ReplyTo = []; /** * An array of all kinds of addresses. * Includes all of $to, $cc, $bcc. * * @see PHPMailer::$to * @see PHPMailer::$cc * @see PHPMailer::$bcc * * @var array */ protected $all_recipients = []; /** * An array of names and addresses queued for validation. * In send(), valid and non duplicate entries are moved to $all_recipients * and one of $to, $cc, or $bcc. * This array is used only for addresses with IDN. * * @see PHPMailer::$to * @see PHPMailer::$cc * @see PHPMailer::$bcc * @see PHPMailer::$all_recipients * * @var array */ protected $RecipientsQueue = []; /** * An array of reply-to names and addresses queued for validation. * In send(), valid and non duplicate entries are moved to $ReplyTo. * This array is used only for addresses with IDN. * * @see PHPMailer::$ReplyTo * * @var array */ protected $ReplyToQueue = []; /** * The array of attachments. * * @var array */ protected $attachment = []; /** * The array of custom headers. * * @var array */ protected $CustomHeader = []; /** * The most recent Message-ID (including angular brackets). * * @var string */ protected $lastMessageID = ''; /** * The message's MIME type. * * @var string */ protected $message_type = ''; /** * The array of MIME boundary strings. * * @var array */ protected $boundary = []; /** * The array of available text strings for the current language. * * @var array */ protected $language = []; /** * The number of errors encountered. * * @var int */ protected $error_count = 0; /** * The S/MIME certificate file path. * * @var string */ protected $sign_cert_file = ''; /** * The S/MIME key file path. * * @var string */ protected $sign_key_file = ''; /** * The optional S/MIME extra certificates ("CA Chain") file path. * * @var string */ protected $sign_extracerts_file = ''; /** * The S/MIME password for the key. * Used only if the key is encrypted. * * @var string */ protected $sign_key_pass = ''; /** * Whether to throw exceptions for errors. * * @var bool */ protected $exceptions = false; /** * Unique ID used for message ID and boundaries. * * @var string */ protected $uniqueid = ''; /** * The PHPMailer Version number. * * @var string */ const VERSION = '6.6.3'; /** * Error severity: message only, continue processing. * * @var int */ const STOP_MESSAGE = 0; /** * Error severity: message, likely ok to continue processing. * * @var int */ const STOP_CONTINUE = 1; /** * Error severity: message, plus full stop, critical error reached. * * @var int */ const STOP_CRITICAL = 2; /** * The SMTP standard CRLF line break. * If you want to change line break format, change static::$LE, not this. */ const CRLF = "\r\n"; /** * "Folding White Space" a white space string used for line folding. */ const FWS = ' '; /** * SMTP RFC standard line ending; Carriage Return, Line Feed. * * @var string */ protected static $LE = self::CRLF; /** * The maximum line length supported by mail(). * * Background: mail() will sometimes corrupt messages * with headers headers longer than 65 chars, see #818. * * @var int */ const MAIL_MAX_LINE_LENGTH = 63; /** * The maximum line length allowed by RFC 2822 section 2.1.1. * * @var int */ const MAX_LINE_LENGTH = 998; /** * The lower maximum line length allowed by RFC 2822 section 2.1.1. * This length does NOT include the line break * 76 means that lines will be 77 or 78 chars depending on whether * the line break format is LF or CRLF; both are valid. * * @var int */ const STD_LINE_LENGTH = 76; /** * Constructor. * * @param bool $exceptions Should we throw external exceptions? */ public function __construct($exceptions = null) { if (null !== $exceptions) { $this->exceptions = (bool) $exceptions; } //Pick an appropriate debug output format automatically $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); } /** * Destructor. */ public function __destruct() { //Close any open SMTP connection nicely $this->smtpClose(); } /** * Call mail() in a safe_mode-aware fashion. * Also, unless sendmail_path points to sendmail (or something that * claims to be sendmail), don't pass params (not a perfect fix, * but it will do). * * @param string $to To * @param string $subject Subject * @param string $body Message Body * @param string $header Additional Header(s) * @param string|null $params Params * * @return bool */ private function mailPassthru($to, $subject, $body, $header, $params) { //Check overloading of mail function to avoid double-encoding if (ini_get('mbstring.func_overload') & 1) { $subject = $this->secureHeader($subject); } else { $subject = $this->encodeHeader($this->secureHeader($subject)); } //Calling mail() with null params breaks $this->edebug('Sending with mail()'); $this->edebug('Sendmail path: ' . ini_get('sendmail_path')); $this->edebug("Envelope sender: {$this->Sender}"); $this->edebug("To: {$to}"); $this->edebug("Subject: {$subject}"); $this->edebug("Headers: {$header}"); if (!$this->UseSendmailOptions || null === $params) { $result = @mail($to, $subject, $body, $header); } else { $this->edebug("Additional params: {$params}"); $result = @mail($to, $subject, $body, $header, $params); } $this->edebug('Result: ' . ($result ? 'true' : 'false')); return $result; } /** * Output debugging info via a user-defined method. * Only generates output if debug output is enabled. * * @see PHPMailer::$Debugoutput * @see PHPMailer::$SMTPDebug * * @param string $str */ protected function edebug($str) { if ($this->SMTPDebug <= 0) { return; } //Is this a PSR-3 logger? if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { $this->Debugoutput->debug($str); return; } //Avoid clash with built-in function names if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { call_user_func($this->Debugoutput, $str, $this->SMTPDebug); return; } switch ($this->Debugoutput) { case 'error_log': //Don't output, just log /** @noinspection ForgottenDebugOutputInspection */ error_log($str); break; case 'html': //Cleans up output a bit for a better looking, HTML-safe output echo htmlentities( preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8' ), "<br>\n"; break; case 'echo': default: //Normalize line breaks $str = preg_replace('/\r\n|\r/m', "\n", $str); echo gmdate('Y-m-d H:i:s'), "\t", //Trim trailing space trim( //Indent for readability, except for trailing break str_replace( "\n", "\n \t ", trim($str) ) ), "\n"; } } /** * Sets message type to HTML or plain. * * @param bool $isHtml True for HTML mode */ public function isHTML($isHtml = true) { if ($isHtml) { $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; } else { $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; } } /** * Send messages using SMTP. */ public function isSMTP() { $this->Mailer = 'smtp'; } /** * Send messages using PHP's mail() function. */ public function isMail() { $this->Mailer = 'mail'; } /** * Send messages using $Sendmail. */ public function isSendmail() { $ini_sendmail_path = ini_get('sendmail_path'); if (false === stripos($ini_sendmail_path, 'sendmail')) { $this->Sendmail = '/usr/sbin/sendmail'; } else { $this->Sendmail = $ini_sendmail_path; } $this->Mailer = 'sendmail'; } /** * Send messages using qmail. */ public function isQmail() { $ini_sendmail_path = ini_get('sendmail_path'); if (false === stripos($ini_sendmail_path, 'qmail')) { $this->Sendmail = '/var/qmail/bin/qmail-inject'; } else { $this->Sendmail = $ini_sendmail_path; } $this->Mailer = 'qmail'; } /** * Add a "To" address. * * @param string $address The email address to send to * @param string $name * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ public function addAddress($address, $name = '') { return $this->addOrEnqueueAnAddress('to', $address, $name); } /** * Add a "CC" address. * * @param string $address The email address to send to * @param string $name * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ public function addCC($address, $name = '') { return $this->addOrEnqueueAnAddress('cc', $address, $name); } /** * Add a "BCC" address. * * @param string $address The email address to send to * @param string $name * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ public function addBCC($address, $name = '') { return $this->addOrEnqueueAnAddress('bcc', $address, $name); } /** * Add a "Reply-To" address. * * @param string $address The email address to reply to * @param string $name * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ public function addReplyTo($address, $name = '') { return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); } /** * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still * be modified after calling this function), addition of such addresses is delayed until send(). * Addresses that have been added already return false, but do not throw exceptions. * * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' * @param string $address The email address * @param string $name An optional username associated with the address * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ protected function addOrEnqueueAnAddress($kind, $address, $name) { $pos = false; if ($address !== null) { $address = trim($address); $pos = strrpos($address, '@'); } if (false === $pos) { //At-sign is missing. $error_message = sprintf( '%s (%s): %s', $this->lang('invalid_address'), $kind, $address ); $this->setError($error_message); $this->edebug($error_message); if ($this->exceptions) { throw new Exception($error_message); } return false; } if ($name !== null) { $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim } else { $name = ''; } $params = [$kind, $address, $name]; //Enqueue addresses with IDN until we know the PHPMailer::$CharSet. //Domain is assumed to be whatever is after the last @ symbol in the address if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) { if ('Reply-To' !== $kind) { if (!array_key_exists($address, $this->RecipientsQueue)) { $this->RecipientsQueue[$address] = $params; return true; } } elseif (!array_key_exists($address, $this->ReplyToQueue)) { $this->ReplyToQueue[$address] = $params; return true; } return false; } //Immediately add standard addresses without IDN. return call_user_func_array([$this, 'addAnAddress'], $params); } /** * Add an address to one of the recipient arrays or to the ReplyTo array. * Addresses that have been added already return false, but do not throw exceptions. * * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' * @param string $address The email address to send, resp. to reply to * @param string $name * * @throws Exception * * @return bool true on success, false if address already used or invalid in some way */ protected function addAnAddress($kind, $address, $name = '') { if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { $error_message = sprintf( '%s: %s', $this->lang('Invalid recipient kind'), $kind ); $this->setError($error_message); $this->edebug($error_message); if ($this->exceptions) { throw new Exception($error_message); } return false; } if (!static::validateAddress($address)) { $error_message = sprintf( '%s (%s): %s', $this->lang('invalid_address'), $kind, $address ); $this->setError($error_message); $this->edebug($error_message); if ($this->exceptions) { throw new Exception($error_message); } return false; } if ('Reply-To' !== $kind) { if (!array_key_exists(strtolower($address), $this->all_recipients)) { $this->{$kind}[] = [$address, $name]; $this->all_recipients[strtolower($address)] = true; return true; } } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { $this->ReplyTo[strtolower($address)] = [$address, $name]; return true; } return false; } /** * Parse and validate a string containing one or more RFC822-style comma-separated email addresses * of the form "display name <address>" into an array of name/address pairs. * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. * Note that quotes in the name part are removed. * * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation * * @param string $addrstr The address list string * @param bool $useimap Whether to use the IMAP extension to parse the list * @param string $charset The charset to use when decoding the address list string. * * @return array */ public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) { $addresses = []; if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { //Use this built-in parser if it's available $list = imap_rfc822_parse_adrlist($addrstr, ''); // Clear any potential IMAP errors to get rid of notices being thrown at end of script. imap_errors(); foreach ($list as $address) { if ( '.SYNTAX-ERROR.' !== $address->host && static::validateAddress($address->mailbox . '@' . $address->host) ) { //Decode the name part if it's present and encoded if ( property_exists($address, 'personal') && //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $address->personal) ) { $origCharset = mb_internal_encoding(); mb_internal_encoding($charset); //Undo any RFC2047-encoded spaces-as-underscores $address->personal = str_replace('_', '=20', $address->personal); //Decode the name $address->personal = mb_decode_mimeheader($address->personal); mb_internal_encoding($origCharset); } $addresses[] = [ 'name' => (property_exists($address, 'personal') ? $address->personal : ''), 'address' => $address->mailbox . '@' . $address->host, ]; } } } else { //Use this simpler parser $list = explode(',', $addrstr); foreach ($list as $address) { $address = trim($address); //Is there a separate name part? if (strpos($address, '<') === false) { //No separate name, just use the whole thing if (static::validateAddress($address)) { $addresses[] = [ 'name' => '', 'address' => $address, ]; } } else { list($name, $email) = explode('<', $address); $email = trim(str_replace('>', '', $email)); $name = trim($name); if (static::validateAddress($email)) { //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled //If this name is encoded, decode it if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { $origCharset = mb_internal_encoding(); mb_internal_encoding($charset); //Undo any RFC2047-encoded spaces-as-underscores $name = str_replace('_', '=20', $name); //Decode the name $name = mb_decode_mimeheader($name); mb_internal_encoding($origCharset); } $addresses[] = [ //Remove any surrounding quotes and spaces from the name 'name' => trim($name, '\'" '), 'address' => $email, ]; } } } } return $addresses; } /** * Set the From and FromName properties. * * @param string $address * @param string $name * @param bool $auto Whether to also set the Sender address, defaults to true * * @throws Exception * * @return bool */ public function setFrom($address, $name = '', $auto = true) { $address = trim($address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim //Don't validate now addresses with IDN. Will be done in send(). $pos = strrpos($address, '@'); if ( (false === $pos) || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported()) && !static::validateAddress($address)) ) { $error_message = sprintf( '%s (From): %s', $this->lang('invalid_address'), $address ); $this->setError($error_message); $this->edebug($error_message); if ($this->exceptions) { throw new Exception($error_message); } return false; } $this->From = $address; $this->FromName = $name; if ($auto && empty($this->Sender)) { $this->Sender = $address; } return true; } /** * Return the Message-ID header of the last email. * Technically this is the value from the last time the headers were created, * but it's also the message ID of the last sent message except in * pathological cases. * * @return string */ public function getLastMessageID() { return $this->lastMessageID; } /** * Check that a string looks like an email address. * Validation patterns supported: * * `auto` Pick best pattern automatically; * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; * * `pcre` Use old PCRE implementation; * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. * * `noregex` Don't use a regex: super fast, really dumb. * Alternatively you may pass in a callable to inject your own validator, for example: * * ```php * PHPMailer::validateAddress('user@example.com', function($address) { * return (strpos($address, '@') !== false); * }); * ``` * * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. * * @param string $address The email address to check * @param string|callable $patternselect Which pattern to use * * @return bool */ public static function validateAddress($address, $patternselect = null) { if (null === $patternselect) { $patternselect = static::$validator; } //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603 if (is_callable($patternselect) && !is_string($patternselect)) { return call_user_func($patternselect, $address); } //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) { return false; } switch ($patternselect) { case 'pcre': //Kept for BC case 'pcre8': /* * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL * is based. * In addition to the addresses allowed by filter_var, also permits: * * dotless domains: `a@b` * * comments: `1234 @ local(blah) .machine .example` * * quoted elements: `'"test blah"@example.org'` * * numeric TLDs: `a@b.123` * * unbracketed IPv4 literals: `a@192.168.0.1` * * IPv6 literals: 'first.last@[IPv6:a1::]' * Not all of these will necessarily work for sending! * * @see http://squiloople.com/2009/12/20/email-address-validation/ * @copyright 2009-2010 Michael Rushton * Feel free to use and redistribute this code. But please keep this copyright notice. */ return (bool) preg_match( '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', $address ); case 'html5': /* * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. * * @see https://html.spec.whatwg.org/#e-mail-state-(type=email) */ return (bool) preg_match( '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', $address ); case 'php': default: return filter_var($address, FILTER_VALIDATE_EMAIL) !== false; } } /** * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the * `intl` and `mbstring` PHP extensions. * * @return bool `true` if required functions for IDN support are present */ public static function idnSupported() { return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding'); } /** * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. * This function silently returns unmodified address if: * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) * - Conversion to punycode is impossible (e.g. required PHP functions are not available) * or fails for any reason (e.g. domain contains characters not allowed in an IDN). * * @see PHPMailer::$CharSet * * @param string $address The email address to convert * * @return string The encoded address in ASCII form */ public function punyencodeAddress($address) { //Verify we have required functions, CharSet, and at-sign. $pos = strrpos($address, '@'); if ( !empty($this->CharSet) && false !== $pos && static::idnSupported() ) { $domain = substr($address, ++$pos); //Verify CharSet string is a valid one, and domain properly encoded in this CharSet. if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) { //Convert the domain from whatever charset it's in to UTF-8 $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet); //Ignore IDE complaints about this line - method signature changed in PHP 5.4 $errorcode = 0; if (defined('INTL_IDNA_VARIANT_UTS46')) { //Use the current punycode standard (appeared in PHP 7.2) $punycode = idn_to_ascii( $domain, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46 ); } elseif (defined('INTL_IDNA_VARIANT_2003')) { //Fall back to this old, deprecated/removed encoding $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003); } else { //Fall back to a default we don't know about $punycode = idn_to_ascii($domain, $errorcode); } if (false !== $punycode) { return substr($address, 0, $pos) . $punycode; } } } return $address; } /** * Create a message and send it. * Uses the sending method specified by $Mailer. * * @throws Exception * * @return bool false on error - See the ErrorInfo property for details of the error */ public function send() { try { if (!$this->preSend()) { return false; } return $this->postSend(); } catch (Exception $exc) { $this->mailHeader = ''; $this->setError($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } } /** * Prepare a message for sending. * * @throws Exception * * @return bool */ public function preSend() { if ( 'smtp' === $this->Mailer || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0)) ) { //SMTP mandates RFC-compliant line endings //and it's also used with mail() on Windows static::setLE(self::CRLF); } else { //Maintain backward compatibility with legacy Linux command line mailers static::setLE(PHP_EOL); } //Check for buggy PHP versions that add a header with an incorrect line break if ( 'mail' === $this->Mailer && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017) || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103)) && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { trigger_error($this->lang('buggy_php'), E_USER_WARNING); } try { $this->error_count = 0; //Reset errors $this->mailHeader = ''; //Dequeue recipient and Reply-To addresses with IDN foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { $params[1] = $this->punyencodeAddress($params[1]); call_user_func_array([$this, 'addAnAddress'], $params); } if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); } //Validate From, Sender, and ConfirmReadingTo addresses foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { $this->{$address_kind} = trim($this->{$address_kind}); if (empty($this->{$address_kind})) { continue; } $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind}); if (!static::validateAddress($this->{$address_kind})) { $error_message = sprintf( '%s (%s): %s', $this->lang('invalid_address'), $address_kind, $this->{$address_kind} ); $this->setError($error_message); $this->edebug($error_message); if ($this->exceptions) { throw new Exception($error_message); } return false; } } //Set whether the message is multipart/alternative if ($this->alternativeExists()) { $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; } $this->setMessageType(); //Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty && empty($this->Body)) { throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); } //Trim subject consistently $this->Subject = trim($this->Subject); //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) $this->MIMEHeader = ''; $this->MIMEBody = $this->createBody(); //createBody may have added some headers, so retain them $tempheaders = $this->MIMEHeader; $this->MIMEHeader = $this->createHeader(); $this->MIMEHeader .= $tempheaders; //To capture the complete message when using mail(), create //an extra header list which createHeader() doesn't fold in if ('mail' === $this->Mailer) { if (count($this->to) > 0) { $this->mailHeader .= $this->addrAppend('To', $this->to); } else { $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); } $this->mailHeader .= $this->headerLine( 'Subject', $this->encodeHeader($this->secureHeader($this->Subject)) ); } //Sign with DKIM if enabled if ( !empty($this->DKIM_domain) && !empty($this->DKIM_selector) && (!empty($this->DKIM_private_string) || (!empty($this->DKIM_private) && static::isPermittedPath($this->DKIM_private) && file_exists($this->DKIM_private) ) ) ) { $header_dkim = $this->DKIM_Add( $this->MIMEHeader . $this->mailHeader, $this->encodeHeader($this->secureHeader($this->Subject)), $this->MIMEBody ); $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE . static::normalizeBreaks($header_dkim) . static::$LE; } return true; } catch (Exception $exc) { $this->setError($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } } /** * Actually send a message via the selected mechanism. * * @throws Exception * * @return bool */ public function postSend() { try { //Choose the mailer and send through it switch ($this->Mailer) { case 'sendmail': case 'qmail': return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); case 'smtp': return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); case 'mail': return $this->mailSend($this->MIMEHeader, $this->MIMEBody); default: $sendMethod = $this->Mailer . 'Send'; if (method_exists($this, $sendMethod)) { return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody); } return $this->mailSend($this->MIMEHeader, $this->MIMEBody); } } catch (Exception $exc) { if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true) { $this->smtp->reset(); } $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } } return false; } /** * Send mail using the $Sendmail program. * * @see PHPMailer::$Sendmail * * @param string $header The message headers * @param string $body The message body * * @throws Exception * * @return bool */ protected function sendmailSend($header, $body) { if ($this->Mailer === 'qmail') { $this->edebug('Sending with qmail'); } else { $this->edebug('Sending with sendmail'); } $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver //A space after `-f` is optional, but there is a long history of its presence //causing problems, so we don't use one //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html //Example problem: https://www.drupal.org/node/1057954 //PHP 5.6 workaround $sendmail_from_value = ini_get('sendmail_from'); if (empty($this->Sender) && !empty($sendmail_from_value)) { //PHP config has a sender address we can use $this->Sender = ini_get('sendmail_from'); } //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { if ($this->Mailer === 'qmail') { $sendmailFmt = '%s -f%s'; } else { $sendmailFmt = '%s -oi -f%s -t'; } } else { //allow sendmail to choose a default envelope sender. It may //seem preferable to force it to use the From header as with //SMTP, but that introduces new problems (see //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and //it has historically worked this way. $sendmailFmt = '%s -oi -t'; } $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); $this->edebug('Sendmail path: ' . $this->Sendmail); $this->edebug('Sendmail command: ' . $sendmail); $this->edebug('Envelope sender: ' . $this->Sender); $this->edebug("Headers: {$header}"); if ($this->SingleTo) { foreach ($this->SingleToArray as $toAddr) { $mail = @popen($sendmail, 'w'); if (!$mail) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); $this->doCallback( ($result === 0), [[$addrinfo['address'], $addrinfo['name']]], $this->cc, $this->bcc, $this->Subject, $body, $this->From, [] ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { $mail = @popen($sendmail, 'w'); if (!$mail) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); $this->doCallback( ($result === 0), $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, [] ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } return true; } /** * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. * * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report * * @param string $string The string to be validated * * @return bool */ protected static function isShellSafe($string) { //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg, //but some hosting providers disable it, creating a security problem that we don't want to have to deal with, //so we don't. if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) { return false; } if ( escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) ) { return false; } $length = strlen($string); for ($i = 0; $i < $length; ++$i) { $c = $string[$i]; //All other characters have a special meaning in at least one common shell, including = and +. //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. //Note that this does permit non-Latin alphanumeric characters based on the current locale. if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { return false; } } return true; } /** * Check whether a file path is of a permitted type. * Used to reject URLs and phar files from functions that access local file paths, * such as addAttachment. * * @param string $path A relative or absolute path to a file * * @return bool */ protected static function isPermittedPath($path) { //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1 return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path); } /** * Check whether a file path is safe, accessible, and readable. * * @param string $path A relative or absolute path to a file * * @return bool */ protected static function fileIsAccessible($path) { if (!static::isPermittedPath($path)) { return false; } $readable = file_exists($path); //If not a UNC path (expected to start with \\), check read permission, see #2069 if (strpos($path, '\\\\') !== 0) { $readable = $readable && is_readable($path); } return $readable; } /** * Send mail using the PHP mail() function. * * @see http://www.php.net/manual/en/book.mail.php * * @param string $header The message headers * @param string $body The message body * * @throws Exception * * @return bool */ protected function mailSend($header, $body) { $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $toArr = []; foreach ($this->to as $toaddr) { $toArr[] = $this->addrFormat($toaddr); } $to = implode(', ', $toArr); $params = null; //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver //A space after `-f` is optional, but there is a long history of its presence //causing problems, so we don't use one //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html //Example problem: https://www.drupal.org/node/1057954 //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. //PHP 5.6 workaround $sendmail_from_value = ini_get('sendmail_from'); if (empty($this->Sender) && !empty($sendmail_from_value)) { //PHP config has a sender address we can use $this->Sender = ini_get('sendmail_from'); } if (!empty($this->Sender) && static::validateAddress($this->Sender)) { if (self::isShellSafe($this->Sender)) { $params = sprintf('-f%s', $this->Sender); } $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $this->Sender); } $result = false; if ($this->SingleTo && count($toArr) > 1) { foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); $this->doCallback( $result, [[$addrinfo['address'], $addrinfo['name']]], $this->cc, $this->bcc, $this->Subject, $body, $this->From, [] ); } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); } if (isset($old_from)) { ini_set('sendmail_from', $old_from); } if (!$result) { throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); } return true; } /** * Get an instance to use for SMTP operations. * Override this function to load your own SMTP implementation, * or set one with setSMTPInstance. * * @return SMTP */ public function getSMTPInstance() { if (!is_object($this->smtp)) { $this->smtp = new SMTP(); } return $this->smtp; } /** * Provide an instance to use for SMTP operations. * * @return SMTP */ public function setSMTPInstance(SMTP $smtp) { $this->smtp = $smtp; return $this->smtp; } /** * Send mail via SMTP. * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. * * @see PHPMailer::setSMTPInstance() to use a different class. * * @uses \PHPMailer\PHPMailer\SMTP * * @param string $header The message headers * @param string $body The message body * * @throws Exception * * @return bool */ protected function smtpSend($header, $body) { $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } //Sender already validated in preSend() if ('' === $this->Sender) { $smtp_from = $this->From; } else { $smtp_from = $this->Sender; } if (!$this->smtp->mail($smtp_from)) { $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); } $callbacks = []; //Attempt to send to all recipients foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { foreach ($togroup as $to) { if (!$this->smtp->recipient($to[0], $this->dsn)) { $error = $this->smtp->getError(); $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; $isSent = false; } else { $isSent = true; } $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]]; } } //Only send the DATA command if we have viable recipients if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); } $smtp_transaction_id = $this->smtp->getLastTransactionID(); if ($this->SMTPKeepAlive) { $this->smtp->reset(); } else { $this->smtp->quit(); $this->smtp->close(); } foreach ($callbacks as $cb) { $this->doCallback( $cb['issent'], [[$cb['to'], $cb['name']]], [], [], $this->Subject, $body, $this->From, ['smtp_transaction_id' => $smtp_transaction_id] ); } //Create error message for any bad addresses if (count($bad_rcpt) > 0) { $errstr = ''; foreach ($bad_rcpt as $bad) { $errstr .= $bad['to'] . ': ' . $bad['error']; } throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); } return true; } /** * Initiate a connection to an SMTP server. * Returns false if the operation failed. * * @param array $options An array of options compatible with stream_context_create() * * @throws Exception * * @uses \PHPMailer\PHPMailer\SMTP * * @return bool */ public function smtpConnect($options = null) { if (null === $this->smtp) { $this->smtp = $this->getSMTPInstance(); } //If no options are provided, use whatever is set in the instance if (null === $options) { $options = $this->SMTPOptions; } //Already connected? if ($this->smtp->connected()) { return true; } $this->smtp->setTimeout($this->Timeout); $this->smtp->setDebugLevel($this->SMTPDebug); $this->smtp->setDebugOutput($this->Debugoutput); $this->smtp->setVerp($this->do_verp); $hosts = explode(';', $this->Host); $lastexception = null; foreach ($hosts as $hostentry) { $hostinfo = []; if ( !preg_match( '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', trim($hostentry), $hostinfo ) ) { $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); //Not a valid host entry continue; } //$hostinfo[1]: optional ssl or tls prefix //$hostinfo[2]: the hostname //$hostinfo[3]: optional port number //The host string prefix can temporarily override the current setting for SMTPSecure //If it's not specified, the default value is used //Check the host name is a valid name or IP address before trying to use it if (!static::isValidHost($hostinfo[2])) { $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; $secure = $this->SMTPSecure; $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) { $prefix = 'ssl://'; $tls = false; //Can't have SSL and TLS at the same time $secure = static::ENCRYPTION_SMTPS; } elseif ('tls' === $hostinfo[1]) { $tls = true; //TLS doesn't use a prefix $secure = static::ENCRYPTION_STARTTLS; } //Do we need the OpenSSL extension? $sslext = defined('OPENSSL_ALGO_SHA256'); if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled if (!$sslext) { throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; $port = $this->Port; if ( array_key_exists(3, $hostinfo) && is_numeric($hostinfo[3]) && $hostinfo[3] > 0 && $hostinfo[3] < 65536 ) { $port = (int) $hostinfo[3]; } if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { try { if ($this->Helo) { $hello = $this->Helo; } else { $hello = $this->serverHostname(); } $this->smtp->hello($hello); //Automatically enable TLS encryption if: //* it's not disabled //* we have openssl extension //* we are not already using SSL //* the server offers STARTTLS if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) { $tls = true; } if ($tls) { if (!$this->smtp->startTLS()) { $message = $this->getSmtpErrorMessage('connect_host'); throw new Exception($message); } //We must resend EHLO after TLS negotiation $this->smtp->hello($hello); } if ( $this->SMTPAuth && !$this->smtp->authenticate( $this->Username, $this->Password, $this->AuthType, $this->oauth ) ) { throw new Exception($this->lang('authenticate')); } return true; } catch (Exception $exc) { $lastexception = $exc; $this->edebug($exc->getMessage()); //We must have connected, but then failed TLS or Auth, so close connection nicely $this->smtp->quit(); } } } //If we get here, all connection attempts have failed, so close connection hard $this->smtp->close(); //As we've caught all exceptions, just report whatever the last one was if ($this->exceptions && null !== $lastexception) { throw $lastexception; } if ($this->exceptions) { // no exception was thrown, likely $this->smtp->connect() failed $message = $this->getSmtpErrorMessage('connect_host'); throw new Exception($message); } return false; } /** * Close the active SMTP session if one exists. */ public function smtpClose() { if ((null !== $this->smtp) && $this->smtp->connected()) { $this->smtp->quit(); $this->smtp->close(); } } /** * Set the language for error messages. * The default language is English. * * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") * Optionally, the language code can be enhanced with a 4-character * script annotation and/or a 2-character country annotation. * @param string $lang_path Path to the language file directory, with trailing separator (slash) * Do not set this from user input! * * @return bool Returns true if the requested language was loaded, false otherwise. */ public function setLanguage($langcode = 'en', $lang_path = '') { //Backwards compatibility for renamed language codes $renamed_langcodes = [ 'br' => 'pt_br', 'cz' => 'cs', 'dk' => 'da', 'no' => 'nb', 'se' => 'sv', 'rs' => 'sr', 'tg' => 'tl', 'am' => 'hy', ]; if (array_key_exists($langcode, $renamed_langcodes)) { $langcode = $renamed_langcodes[$langcode]; } //Define full set of translatable strings in English $PHPMAILER_LANG = [ 'authenticate' => 'SMTP Error: Could not authenticate.', 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' . ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', 'data_not_accepted' => 'SMTP Error: data not accepted.', 'empty_message' => 'Message body empty', 'encoding' => 'Unknown encoding: ', 'execute' => 'Could not execute: ', 'extension_missing' => 'Extension missing: ', 'file_access' => 'Could not access file: ', 'file_open' => 'File Error: Could not open file: ', 'from_failed' => 'The following From address failed: ', 'instantiate' => 'Could not instantiate mail function.', 'invalid_address' => 'Invalid address: ', 'invalid_header' => 'Invalid header name or value', 'invalid_hostentry' => 'Invalid hostentry: ', 'invalid_host' => 'Invalid host: ', 'mailer_not_supported' => ' mailer is not supported.', 'provide_address' => 'You must provide at least one recipient email address.', 'recipients_failed' => 'SMTP Error: The following recipients failed: ', 'signing' => 'Signing Error: ', 'smtp_code' => 'SMTP code: ', 'smtp_code_ex' => 'Additional SMTP info: ', 'smtp_connect_failed' => 'SMTP connect() failed.', 'smtp_detail' => 'Detail: ', 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; } //Validate $langcode $foundlang = true; $langcode = strtolower($langcode); if ( !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches) && $langcode !== 'en' ) { $foundlang = false; $langcode = 'en'; } //There is no English translation file if ('en' !== $langcode) { $langcodes = []; if (!empty($matches['script']) && !empty($matches['country'])) { $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country']; } if (!empty($matches['country'])) { $langcodes[] = $matches['lang'] . $matches['country']; } if (!empty($matches['script'])) { $langcodes[] = $matches['lang'] . $matches['script']; } $langcodes[] = $matches['lang']; //Try and find a readable language file for the requested language. $foundFile = false; foreach ($langcodes as $code) { $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php'; if (static::fileIsAccessible($lang_file)) { $foundFile = true; break; } } if ($foundFile === false) { $foundlang = false; } else { $lines = file($lang_file); foreach ($lines as $line) { //Translation file lines look like this: //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.'; //These files are parsed as text and not PHP so as to avoid the possibility of code injection //See https://blog.stevenlevithan.com/archives/match-quoted-string $matches = []; if ( preg_match( '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/', $line, $matches ) && //Ignore unknown translation keys array_key_exists($matches[1], $PHPMAILER_LANG) ) { //Overwrite language-specific strings so we'll never have missing translation keys. $PHPMAILER_LANG[$matches[1]] = (string)$matches[3]; } } } } $this->language = $PHPMAILER_LANG; return $foundlang; //Returns false if language not found } /** * Get the array of strings for the current language. * * @return array */ public function getTranslations() { if (empty($this->language)) { $this->setLanguage(); // Set the default language. } return $this->language; } /** * Create recipient headers. * * @param string $type * @param array $addr An array of recipients, * where each recipient is a 2-element indexed array with element 0 containing an address * and element 1 containing a name, like: * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']] * * @return string */ public function addrAppend($type, $addr) { $addresses = []; foreach ($addr as $address) { $addresses[] = $this->addrFormat($address); } return $type . ': ' . implode(', ', $addresses) . static::$LE; } /** * Format an address for use in a message header. * * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like * ['joe@example.com', 'Joe User'] * * @return string */ public function addrFormat($addr) { if (empty($addr[1])) { //No name provided return $this->secureHeader($addr[0]); } return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader($addr[0]) . '>'; } /** * Word-wrap message. * For use with mailers that do not automatically perform wrapping * and for quoted-printable encoded messages. * Original written by philippe. * * @param string $message The message to wrap * @param int $length The line length to wrap to * @param bool $qp_mode Whether to run in Quoted-Printable mode * * @return string */ public function wrapText($message, $length, $qp_mode = false) { if ($qp_mode) { $soft_break = sprintf(' =%s', static::$LE); } else { $soft_break = static::$LE; } //If utf-8 encoding is used, we will need to make sure we don't //split multibyte characters when we wrap $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); $lelen = strlen(static::$LE); $crlflen = strlen(static::$LE); $message = static::normalizeBreaks($message); //Remove a trailing line break if (substr($message, -$lelen) === static::$LE) { $message = substr($message, 0, -$lelen); } //Split message into lines $lines = explode(static::$LE, $message); //Message will be rebuilt in here $message = ''; foreach ($lines as $line) { $words = explode(' ', $line); $buf = ''; $firstword = true; foreach ($words as $word) { if ($qp_mode && (strlen($word) > $length)) { $space_left = $length - strlen($buf) - $crlflen; if (!$firstword) { if ($space_left > 20) { $len = $space_left; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); } elseif ('=' === substr($word, $len - 1, 1)) { --$len; } elseif ('=' === substr($word, $len - 2, 1)) { $len -= 2; } $part = substr($word, 0, $len); $word = substr($word, $len); $buf .= ' ' . $part; $message .= $buf . sprintf('=%s', static::$LE); } else { $message .= $buf . $soft_break; } $buf = ''; } while ($word !== '') { if ($length <= 0) { break; } $len = $length; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); } elseif ('=' === substr($word, $len - 1, 1)) { --$len; } elseif ('=' === substr($word, $len - 2, 1)) { $len -= 2; } $part = substr($word, 0, $len); $word = (string) substr($word, $len); if ($word !== '') { $message .= $part . sprintf('=%s', static::$LE); } else { $buf = $part; } } } else { $buf_o = $buf; if (!$firstword) { $buf .= ' '; } $buf .= $word; if ('' !== $buf_o && strlen($buf) > $length) { $message .= $buf_o . $soft_break; $buf = $word; } } $firstword = false; } $message .= $buf . static::$LE; } return $message; } /** * Find the last character boundary prior to $maxLength in a utf-8 * quoted-printable encoded string. * Original written by Colin Brown. * * @param string $encodedText utf-8 QP text * @param int $maxLength Find the last character boundary prior to this length * * @return int */ public function utf8CharBoundary($encodedText, $maxLength) { $foundSplitPos = false; $lookBack = 3; while (!$foundSplitPos) { $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); $encodedCharPos = strpos($lastChunk, '='); if (false !== $encodedCharPos) { //Found start of encoded character byte within $lookBack block. //Check the encoded byte value (the 2 chars after the '=') $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); $dec = hexdec($hex); if ($dec < 128) { //Single byte character. //If the encoded char was found at pos 0, it will fit //otherwise reduce maxLength to start of the encoded char if ($encodedCharPos > 0) { $maxLength -= $lookBack - $encodedCharPos; } $foundSplitPos = true; } elseif ($dec >= 192) { //First byte of a multi byte character //Reduce maxLength to split at start of character $maxLength -= $lookBack - $encodedCharPos; $foundSplitPos = true; } elseif ($dec < 192) { //Middle byte of a multi byte character, look further back $lookBack += 3; } } else { //No encoded character found $foundSplitPos = true; } } return $maxLength; } /** * Apply word wrapping to the message body. * Wraps the message body to the number of chars set in the WordWrap property. * You should only do this to plain-text bodies as wrapping HTML tags may break them. * This is called automatically by createBody(), so you don't need to call it yourself. */ public function setWordWrap() { if ($this->WordWrap < 1) { return; } switch ($this->message_type) { case 'alt': case 'alt_inline': case 'alt_attach': case 'alt_inline_attach': $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); break; default: $this->Body = $this->wrapText($this->Body, $this->WordWrap); break; } } /** * Assemble message headers. * * @return string The assembled headers */ public function createHeader() { $result = ''; $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate); //The To header is created automatically by mail(), so needs to be omitted here if ('mail' !== $this->Mailer) { if ($this->SingleTo) { foreach ($this->to as $toaddr) { $this->SingleToArray[] = $this->addrFormat($toaddr); } } elseif (count($this->to) > 0) { $result .= $this->addrAppend('To', $this->to); } elseif (count($this->cc) === 0) { $result .= $this->headerLine('To', 'undisclosed-recipients:;'); } } $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]); //sendmail and mail() extract Cc from the header before sending if (count($this->cc) > 0) { $result .= $this->addrAppend('Cc', $this->cc); } //sendmail and mail() extract Bcc from the header before sending if ( ( 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer ) && count($this->bcc) > 0 ) { $result .= $this->addrAppend('Bcc', $this->bcc); } if (count($this->ReplyTo) > 0) { $result .= $this->addrAppend('Reply-To', $this->ReplyTo); } //mail() sets the subject itself if ('mail' !== $this->Mailer) { $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); } //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 //https://tools.ietf.org/html/rfc5322#section-3.6.4 if ( '' !== $this->MessageID && preg_match( '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' . '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' . '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' . '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' . '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di', $this->MessageID ) ) { $this->lastMessageID = $this->MessageID; } else { $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); } $result .= $this->headerLine('Message-ID', $this->lastMessageID); if (null !== $this->Priority) { $result .= $this->headerLine('X-Priority', $this->Priority); } if ('' === $this->XMailer) { //Empty string for default X-Mailer header $result .= $this->headerLine( 'X-Mailer', 'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)' ); } elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') { //Some string $result .= $this->headerLine('X-Mailer', trim($this->XMailer)); } //Other values result in no X-Mailer header if ('' !== $this->ConfirmReadingTo) { $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); } //Add custom headers foreach ($this->CustomHeader as $header) { $result .= $this->headerLine( trim($header[0]), $this->encodeHeader(trim($header[1])) ); } if (!$this->sign_key_file) { $result .= $this->headerLine('MIME-Version', '1.0'); $result .= $this->getMailMIME(); } return $result; } /** * Get the message MIME type headers. * * @return string */ public function getMailMIME() { $result = ''; $ismultipart = true; switch ($this->message_type) { case 'inline': $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); break; case 'attach': case 'inline_attach': case 'alt_attach': case 'alt_inline_attach': $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';'); $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); break; case 'alt': case 'alt_inline': $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); break; default: //Catches case 'plain': and case '': $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); $ismultipart = false; break; } //RFC1341 part 5 says 7bit is assumed if not specified if (static::ENCODING_7BIT !== $this->Encoding) { //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE if ($ismultipart) { if (static::ENCODING_8BIT === $this->Encoding) { $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT); } //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible } else { $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); } } return $result; } /** * Returns the whole MIME message. * Includes complete headers and body. * Only valid post preSend(). * * @see PHPMailer::preSend() * * @return string */ public function getSentMIMEMessage() { return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) . static::$LE . static::$LE . $this->MIMEBody; } /** * Create a unique ID to use for boundaries. * * @return string */ protected function generateId() { $len = 32; //32 bytes = 256 bits $bytes = ''; if (function_exists('random_bytes')) { try { $bytes = random_bytes($len); } catch (\Exception $e) { //Do nothing } } elseif (function_exists('openssl_random_pseudo_bytes')) { /** @noinspection CryptographicallySecureRandomnessInspection */ $bytes = openssl_random_pseudo_bytes($len); } if ($bytes === '') { //We failed to produce a proper random string, so make do. //Use a hash to force the length to the same as the other methods $bytes = hash('sha256', uniqid((string) mt_rand(), true), true); } //We don't care about messing up base64 format here, just want a random string return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true))); } /** * Assemble the message body. * Returns an empty string on failure. * * @throws Exception * * @return string The assembled message body */ public function createBody() { $body = ''; //Create unique IDs and preset boundaries $this->uniqueid = $this->generateId(); $this->boundary[1] = 'b1_' . $this->uniqueid; $this->boundary[2] = 'b2_' . $this->uniqueid; $this->boundary[3] = 'b3_' . $this->uniqueid; if ($this->sign_key_file) { $body .= $this->getMailMIME() . static::$LE; } $this->setWordWrap(); $bodyEncoding = $this->Encoding; $bodyCharSet = $this->CharSet; //Can we do a 7-bit downgrade? if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) { $bodyEncoding = static::ENCODING_7BIT; //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit $bodyCharSet = static::CHARSET_ASCII; } //If lines are too long, and we're not already using an encoding that will shorten them, //change to quoted-printable transfer encoding for the body part only if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) { $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } $altBodyEncoding = $this->Encoding; $altBodyCharSet = $this->CharSet; //Can we do a 7-bit downgrade? if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) { $altBodyEncoding = static::ENCODING_7BIT; //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit $altBodyCharSet = static::CHARSET_ASCII; } //If lines are too long, and we're not already using an encoding that will shorten them, //change to quoted-printable transfer encoding for the alt body part only if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } //Use this as a preamble in all multipart message types $mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE; switch ($this->message_type) { case 'inline': $body .= $mimepre; $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; $body .= $this->attachAll('inline', $this->boundary[1]); break; case 'attach': $body .= $mimepre; $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'inline_attach': $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";'); $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); $body .= static::$LE; $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= static::$LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt': $body .= $mimepre; $body .= $this->getBoundary( $this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding ); $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[1], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding ); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; if (!empty($this->Ical)) { $method = static::ICAL_METHOD_REQUEST; foreach (static::$IcalMethods as $imethod) { if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) { $method = $imethod; break; } } $body .= $this->getBoundary( $this->boundary[1], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method, '' ); $body .= $this->encodeString($this->Ical, $this->Encoding); $body .= static::$LE; } $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_inline': $body .= $mimepre; $body .= $this->getBoundary( $this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding ); $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= static::$LE; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";'); $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding ); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= static::$LE; $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_attach': $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"'); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding ); $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding ); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; if (!empty($this->Ical)) { $method = static::ICAL_METHOD_REQUEST; foreach (static::$IcalMethods as $imethod) { if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) { $method = $imethod; break; } } $body .= $this->getBoundary( $this->boundary[2], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method, '' ); $body .= $this->encodeString($this->Ical, $this->Encoding); } $body .= $this->endBoundary($this->boundary[2]); $body .= static::$LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt_inline_attach': $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"'); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding ); $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= static::$LE; $body .= $this->textLine('--' . $this->boundary[2]); $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";'); $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); $body .= static::$LE; $body .= $this->getBoundary( $this->boundary[3], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding ); $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= static::$LE; $body .= $this->attachAll('inline', $this->boundary[3]); $body .= static::$LE; $body .= $this->endBoundary($this->boundary[2]); $body .= static::$LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; default: //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types //Reset the `Encoding` property in case we changed it for line length reasons $this->Encoding = $bodyEncoding; $body .= $this->encodeString($this->Body, $this->Encoding); break; } if ($this->isError()) { $body = ''; if ($this->exceptions) { throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); } } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { throw new Exception($this->lang('extension_missing') . 'openssl'); } $file = tempnam(sys_get_temp_dir(), 'srcsign'); $signed = tempnam(sys_get_temp_dir(), 'mailsign'); file_put_contents($file, $body); //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 if (empty($this->sign_extracerts_file)) { $sign = @openssl_pkcs7_sign( $file, $signed, 'file://' . realpath($this->sign_cert_file), ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], [] ); } else { $sign = @openssl_pkcs7_sign( $file, $signed, 'file://' . realpath($this->sign_cert_file), ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], [], PKCS7_DETACHED, $this->sign_extracerts_file ); } @unlink($file); if ($sign) { $body = file_get_contents($signed); @unlink($signed); //The message returned by openssl contains both headers and body, so need to split them up $parts = explode("\n\n", $body, 2); $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; $body = $parts[1]; } else { @unlink($signed); throw new Exception($this->lang('signing') . openssl_error_string()); } } catch (Exception $exc) { $body = ''; if ($this->exceptions) { throw $exc; } } } return $body; } /** * Return the start of a message boundary. * * @param string $boundary * @param string $charSet * @param string $contentType * @param string $encoding * * @return string */ protected function getBoundary($boundary, $charSet, $contentType, $encoding) { $result = ''; if ('' === $charSet) { $charSet = $this->CharSet; } if ('' === $contentType) { $contentType = $this->ContentType; } if ('' === $encoding) { $encoding = $this->Encoding; } $result .= $this->textLine('--' . $boundary); $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); $result .= static::$LE; //RFC1341 part 5 says 7bit is assumed if not specified if (static::ENCODING_7BIT !== $encoding) { $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); } $result .= static::$LE; return $result; } /** * Return the end of a message boundary. * * @param string $boundary * * @return string */ protected function endBoundary($boundary) { return static::$LE . '--' . $boundary . '--' . static::$LE; } /** * Set the message type. * PHPMailer only supports some preset message types, not arbitrary MIME structures. */ protected function setMessageType() { $type = []; if ($this->alternativeExists()) { $type[] = 'alt'; } if ($this->inlineImageExists()) { $type[] = 'inline'; } if ($this->attachmentExists()) { $type[] = 'attach'; } $this->message_type = implode('_', $type); if ('' === $this->message_type) { //The 'plain' message_type refers to the message having a single body element, not that it is plain-text $this->message_type = 'plain'; } } /** * Format a header line. * * @param string $name * @param string|int $value * * @return string */ public function headerLine($name, $value) { return $name . ': ' . $value . static::$LE; } /** * Return a formatted mail line. * * @param string $value * * @return string */ public function textLine($value) { return $value . static::$LE; } /** * Add an attachment from a path on the filesystem. * Never use a user-supplied path to a file! * Returns false if the file could not be found or read. * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client. * If you need to do that, fetch the resource yourself and pass it in via a local file or string. * * @param string $path Path to the attachment * @param string $name Overrides the attachment name * @param string $encoding File encoding (see $Encoding) * @param string $type MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified * @param string $disposition Disposition to use * * @throws Exception * * @return bool */ public function addAttachment( $path, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'attachment' ) { try { if (!static::fileIsAccessible($path)) { throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name if ('' === $type) { $type = static::filenameToType($path); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); if ('' === $name) { $name = $filename; } if (!$this->validateEncoding($encoding)) { throw new Exception($this->lang('encoding') . $encoding); } $this->attachment[] = [ 0 => $path, 1 => $filename, 2 => $name, 3 => $encoding, 4 => $type, 5 => false, //isStringAttachment 6 => $disposition, 7 => $name, ]; } catch (Exception $exc) { $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } return true; } /** * Return the array of attachments. * * @return array */ public function getAttachments() { return $this->attachment; } /** * Attach all file, string, and binary attachments to the message. * Returns an empty string on failure. * * @param string $disposition_type * @param string $boundary * * @throws Exception * * @return string */ protected function attachAll($disposition_type, $boundary) { //Return text of body $mime = []; $cidUniq = []; $incl = []; //Add all attachments foreach ($this->attachment as $attachment) { //Check if it is a valid disposition_filter if ($attachment[6] === $disposition_type) { //Check for string attachment $string = ''; $path = ''; $bString = $attachment[5]; if ($bString) { $string = $attachment[0]; } else { $path = $attachment[0]; } $inclhash = hash('sha256', serialize($attachment)); if (in_array($inclhash, $incl, true)) { continue; } $incl[] = $inclhash; $name = $attachment[2]; $encoding = $attachment[3]; $type = $attachment[4]; $disposition = $attachment[6]; $cid = $attachment[7]; if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) { continue; } $cidUniq[$cid] = true; $mime[] = sprintf('--%s%s', $boundary, static::$LE); //Only include a filename property if we have one if (!empty($name)) { $mime[] = sprintf( 'Content-Type: %s; name=%s%s', $type, static::quotedString($this->encodeHeader($this->secureHeader($name))), static::$LE ); } else { $mime[] = sprintf( 'Content-Type: %s%s', $type, static::$LE ); } //RFC1341 part 5 says 7bit is assumed if not specified if (static::ENCODING_7BIT !== $encoding) { $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE); } //Only set Content-IDs on inline attachments if ((string) $cid !== '' && $disposition === 'inline') { $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE; } //Allow for bypassing the Content-Disposition header if (!empty($disposition)) { $encoded_name = $this->encodeHeader($this->secureHeader($name)); if (!empty($encoded_name)) { $mime[] = sprintf( 'Content-Disposition: %s; filename=%s%s', $disposition, static::quotedString($encoded_name), static::$LE . static::$LE ); } else { $mime[] = sprintf( 'Content-Disposition: %s%s', $disposition, static::$LE . static::$LE ); } } else { $mime[] = static::$LE; } //Encode as string attachment if ($bString) { $mime[] = $this->encodeString($string, $encoding); } else { $mime[] = $this->encodeFile($path, $encoding); } if ($this->isError()) { return ''; } $mime[] = static::$LE; } } $mime[] = sprintf('--%s--%s', $boundary, static::$LE); return implode('', $mime); } /** * Encode a file attachment in requested format. * Returns an empty string on failure. * * @param string $path The full path to the file * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' * * @return string */ protected function encodeFile($path, $encoding = self::ENCODING_BASE64) { try { if (!static::fileIsAccessible($path)) { throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = file_get_contents($path); if (false === $file_buffer) { throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = $this->encodeString($file_buffer, $encoding); return $file_buffer; } catch (Exception $exc) { $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } return ''; } } /** * Encode a string in requested format. * Returns an empty string on failure. * * @param string $str The text to encode * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' * * @throws Exception * * @return string */ public function encodeString($str, $encoding = self::ENCODING_BASE64) { $encoded = ''; switch (strtolower($encoding)) { case static::ENCODING_BASE64: $encoded = chunk_split( base64_encode($str), static::STD_LINE_LENGTH, static::$LE ); break; case static::ENCODING_7BIT: case static::ENCODING_8BIT: $encoded = static::normalizeBreaks($str); //Make sure it ends with a line break if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) { $encoded .= static::$LE; } break; case static::ENCODING_BINARY: $encoded = $str; break; case static::ENCODING_QUOTED_PRINTABLE: $encoded = $this->encodeQP($str); break; default: $this->setError($this->lang('encoding') . $encoding); if ($this->exceptions) { throw new Exception($this->lang('encoding') . $encoding); } break; } return $encoded; } /** * Encode a header value (not including its label) optimally. * Picks shortest of Q, B, or none. Result includes folding if needed. * See RFC822 definitions for phrase, comment and text positions. * * @param string $str The header value to encode * @param string $position What context the string will be used in * * @return string */ public function encodeHeader($str, $position = 'text') { $matchcount = 0; switch (strtolower($position)) { case 'phrase': if (!preg_match('/[\200-\377]/', $str)) { //Can't use addslashes as we don't know the value of magic_quotes_sybase $encoded = addcslashes($str, "\0..\37\177\\\""); if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { return $encoded; } return "\"$encoded\""; } $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); break; /* @noinspection PhpMissingBreakStatementInspection */ case 'comment': $matchcount = preg_match_all('/[()"]/', $str, $matches); //fallthrough case 'text': default: $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); break; } if ($this->has8bitChars($str)) { $charset = $this->CharSet; } else { $charset = static::CHARSET_ASCII; } //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`"). $overhead = 8 + strlen($charset); if ('mail' === $this->Mailer) { $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead; } else { $maxlen = static::MAX_LINE_LENGTH - $overhead; } //Select the encoding that produces the shortest output and/or prevents corruption. if ($matchcount > strlen($str) / 3) { //More than 1/3 of the content needs encoding, use B-encode. $encoding = 'B'; } elseif ($matchcount > 0) { //Less than 1/3 of the content needs encoding, use Q-encode. $encoding = 'Q'; } elseif (strlen($str) > $maxlen) { //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption. $encoding = 'Q'; } else { //No reformatting needed $encoding = false; } switch ($encoding) { case 'B': if ($this->hasMultiBytes($str)) { //Use a custom function which correctly encodes and wraps long //multibyte strings without breaking lines within a character $encoded = $this->base64EncodeWrapMB($str, "\n"); } else { $encoded = base64_encode($str); $maxlen -= $maxlen % 4; $encoded = trim(chunk_split($encoded, $maxlen, "\n")); } $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded); break; case 'Q': $encoded = $this->encodeQ($str, $position); $encoded = $this->wrapText($encoded, $maxlen, true); $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded); break; default: return $str; } return trim(static::normalizeBreaks($encoded)); } /** * Check if a string contains multi-byte characters. * * @param string $str multi-byte text to wrap encode * * @return bool */ public function hasMultiBytes($str) { if (function_exists('mb_strlen')) { return strlen($str) > mb_strlen($str, $this->CharSet); } //Assume no multibytes (we can't handle without mbstring functions anyway) return false; } /** * Does a string contain any 8-bit chars (in any charset)? * * @param string $text * * @return bool */ public function has8bitChars($text) { return (bool) preg_match('/[\x80-\xFF]/', $text); } /** * Encode and wrap long multibyte strings for mail headers * without breaking lines within a character. * Adapted from a function by paravoid. * * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 * * @param string $str multi-byte text to wrap encode * @param string $linebreak string to use as linefeed/end-of-line * * @return string */ public function base64EncodeWrapMB($str, $linebreak = null) { $start = '=?' . $this->CharSet . '?B?'; $end = '?='; $encoded = ''; if (null === $linebreak) { $linebreak = static::$LE; } $mb_length = mb_strlen($str, $this->CharSet); //Each line must have length <= 75, including $start and $end $length = 75 - strlen($start) - strlen($end); //Average multi-byte ratio $ratio = $mb_length / strlen($str); //Base64 has a 4:3 ratio $avgLength = floor($length * $ratio * .75); $offset = 0; for ($i = 0; $i < $mb_length; $i += $offset) { $lookBack = 0; do { $offset = $avgLength - $lookBack; $chunk = mb_substr($str, $i, $offset, $this->CharSet); $chunk = base64_encode($chunk); ++$lookBack; } while (strlen($chunk) > $length); $encoded .= $chunk . $linebreak; } //Chomp the last linefeed return substr($encoded, 0, -strlen($linebreak)); } /** * Encode a string in quoted-printable format. * According to RFC2045 section 6.7. * * @param string $string The text to encode * * @return string */ public function encodeQP($string) { return static::normalizeBreaks(quoted_printable_encode($string)); } /** * Encode a string using Q encoding. * * @see http://tools.ietf.org/html/rfc2047#section-4.2 * * @param string $str the text to encode * @param string $position Where the text is going to be used, see the RFC for what that means * * @return string */ public function encodeQ($str, $position = 'text') { //There should not be any EOL in the string $pattern = ''; $encoded = str_replace(["\r", "\n"], '', $str); switch (strtolower($position)) { case 'phrase': //RFC 2047 section 5.3 $pattern = '^A-Za-z0-9!*+\/ -'; break; /* * RFC 2047 section 5.2. * Build $pattern without including delimiters and [] */ /* @noinspection PhpMissingBreakStatementInspection */ case 'comment': $pattern = '\(\)"'; /* Intentional fall through */ case 'text': default: //RFC 2047 section 5.1 //Replace every high ascii, control, =, ? and _ characters $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; break; } $matches = []; if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { //If the string contains an '=', make sure it's the first thing we replace //so as to avoid double-encoding $eqkey = array_search('=', $matches[0], true); if (false !== $eqkey) { unset($matches[0][$eqkey]); array_unshift($matches[0], '='); } foreach (array_unique($matches[0]) as $char) { $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); } } //Replace spaces with _ (more readable than =20) //RFC 2047 section 4.2(2) return str_replace(' ', '_', $encoded); } /** * Add a string or binary attachment (non-filesystem). * This method can be used to attach ascii or binary data, * such as a BLOB record from a database. * * @param string $string String attachment data * @param string $filename Name of the attachment * @param string $encoding File encoding (see $Encoding) * @param string $type File extension (MIME) type * @param string $disposition Disposition to use * * @throws Exception * * @return bool True on successfully adding an attachment */ public function addStringAttachment( $string, $filename, $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'attachment' ) { try { //If a MIME type is not specified, try to work it out from the file name if ('' === $type) { $type = static::filenameToType($filename); } if (!$this->validateEncoding($encoding)) { throw new Exception($this->lang('encoding') . $encoding); } //Append to $attachment array $this->attachment[] = [ 0 => $string, 1 => $filename, 2 => static::mb_pathinfo($filename, PATHINFO_BASENAME), 3 => $encoding, 4 => $type, 5 => true, //isStringAttachment 6 => $disposition, 7 => 0, ]; } catch (Exception $exc) { $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } return true; } /** * Add an embedded (inline) attachment from a file. * This can include images, sounds, and just about any other document type. * These differ from 'regular' attachments in that they are intended to be * displayed inline with the message, not just attached for download. * This is used in HTML messages that embed the images * the HTML refers to using the `$cid` value in `img` tags, for example `<img src="cid:mylogo">`. * Never use a user-supplied path to a file! * * @param string $path Path to the attachment * @param string $cid Content ID of the attachment; Use this to reference * the content when using an embedded image in HTML * @param string $name Overrides the attachment filename * @param string $encoding File encoding (see $Encoding) defaults to `base64` * @param string $type File MIME type (by default mapped from the `$path` filename's extension) * @param string $disposition Disposition to use: `inline` (default) or `attachment` * (unlikely you want this – {@see `addAttachment()`} instead) * * @return bool True on successfully adding an attachment * @throws Exception * */ public function addEmbeddedImage( $path, $cid, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'inline' ) { try { if (!static::fileIsAccessible($path)) { throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name if ('' === $type) { $type = static::filenameToType($path); } if (!$this->validateEncoding($encoding)) { throw new Exception($this->lang('encoding') . $encoding); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); if ('' === $name) { $name = $filename; } //Append to $attachment array $this->attachment[] = [ 0 => $path, 1 => $filename, 2 => $name, 3 => $encoding, 4 => $type, 5 => false, //isStringAttachment 6 => $disposition, 7 => $cid, ]; } catch (Exception $exc) { $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } return true; } /** * Add an embedded stringified attachment. * This can include images, sounds, and just about any other document type. * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type. * * @param string $string The attachment binary data * @param string $cid Content ID of the attachment; Use this to reference * the content when using an embedded image in HTML * @param string $name A filename for the attachment. If this contains an extension, * PHPMailer will attempt to set a MIME type for the attachment. * For example 'file.jpg' would get an 'image/jpeg' MIME type. * @param string $encoding File encoding (see $Encoding), defaults to 'base64' * @param string $type MIME type - will be used in preference to any automatically derived type * @param string $disposition Disposition to use * * @throws Exception * * @return bool True on successfully adding an attachment */ public function addStringEmbeddedImage( $string, $cid, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'inline' ) { try { //If a MIME type is not specified, try to work it out from the name if ('' === $type && !empty($name)) { $type = static::filenameToType($name); } if (!$this->validateEncoding($encoding)) { throw new Exception($this->lang('encoding') . $encoding); } //Append to $attachment array $this->attachment[] = [ 0 => $string, 1 => $name, 2 => $name, 3 => $encoding, 4 => $type, 5 => true, //isStringAttachment 6 => $disposition, 7 => $cid, ]; } catch (Exception $exc) { $this->setError($exc->getMessage()); $this->edebug($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } return true; } /** * Validate encodings. * * @param string $encoding * * @return bool */ protected function validateEncoding($encoding) { return in_array( $encoding, [ self::ENCODING_7BIT, self::ENCODING_QUOTED_PRINTABLE, self::ENCODING_BASE64, self::ENCODING_8BIT, self::ENCODING_BINARY, ], true ); } /** * Check if an embedded attachment is present with this cid. * * @param string $cid * * @return bool */ protected function cidExists($cid) { foreach ($this->attachment as $attachment) { if ('inline' === $attachment[6] && $cid === $attachment[7]) { return true; } } return false; } /** * Check if an inline attachment is present. * * @return bool */ public function inlineImageExists() { foreach ($this->attachment as $attachment) { if ('inline' === $attachment[6]) { return true; } } return false; } /** * Check if an attachment (non-inline) is present. * * @return bool */ public function attachmentExists() { foreach ($this->attachment as $attachment) { if ('attachment' === $attachment[6]) { return true; } } return false; } /** * Check if this message has an alternative body set. * * @return bool */ public function alternativeExists() { return !empty($this->AltBody); } /** * Clear queued addresses of given kind. * * @param string $kind 'to', 'cc', or 'bcc' */ public function clearQueuedAddresses($kind) { $this->RecipientsQueue = array_filter( $this->RecipientsQueue, static function ($params) use ($kind) { return $params[0] !== $kind; } ); } /** * Clear all To recipients. */ public function clearAddresses() { foreach ($this->to as $to) { unset($this->all_recipients[strtolower($to[0])]); } $this->to = []; $this->clearQueuedAddresses('to'); } /** * Clear all CC recipients. */ public function clearCCs() { foreach ($this->cc as $cc) { unset($this->all_recipients[strtolower($cc[0])]); } $this->cc = []; $this->clearQueuedAddresses('cc'); } /** * Clear all BCC recipients. */ public function clearBCCs() { foreach ($this->bcc as $bcc) { unset($this->all_recipients[strtolower($bcc[0])]); } $this->bcc = []; $this->clearQueuedAddresses('bcc'); } /** * Clear all ReplyTo recipients. */ public function clearReplyTos() { $this->ReplyTo = []; $this->ReplyToQueue = []; } /** * Clear all recipient types. */ public function clearAllRecipients() { $this->to = []; $this->cc = []; $this->bcc = []; $this->all_recipients = []; $this->RecipientsQueue = []; } /** * Clear all filesystem, string, and binary attachments. */ public function clearAttachments() { $this->attachment = []; } /** * Clear all custom headers. */ public function clearCustomHeaders() { $this->CustomHeader = []; } /** * Add an error message to the error container. * * @param string $msg */ protected function setError($msg) { ++$this->error_count; if ('smtp' === $this->Mailer && null !== $this->smtp) { $lasterror = $this->smtp->getError(); if (!empty($lasterror['error'])) { $msg .= $this->lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } $this->ErrorInfo = $msg; } /** * Return an RFC 822 formatted date. * * @return string */ public static function rfcDate() { //Set the time zone to whatever the default is to avoid 500 errors //Will default to UTC if it's not set properly in php.ini date_default_timezone_set(@date_default_timezone_get()); return date('D, j M Y H:i:s O'); } /** * Get the server hostname. * Returns 'localhost.localdomain' if unknown. * * @return string */ protected function serverHostname() { $result = ''; if (!empty($this->Hostname)) { $result = $this->Hostname; } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) { $result = $_SERVER['SERVER_NAME']; } elseif (function_exists('gethostname') && gethostname() !== false) { $result = gethostname(); } elseif (php_uname('n') !== false) { $result = php_uname('n'); } if (!static::isValidHost($result)) { return 'localhost.localdomain'; } return $result; } /** * Validate whether a string contains a valid value to use as a hostname or IP address. * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`. * * @param string $host The host name or IP address to check * * @return bool */ public static function isValidHost($host) { //Simple syntax limits if ( empty($host) || !is_string($host) || strlen($host) > 256 || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host) ) { return false; } //Looks like a bracketed IPv6 address if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') { return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; } //If removing all the dots results in a numeric string, it must be an IPv4 address. //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names if (is_numeric(str_replace('.', '', $host))) { //Is it a valid IPv4 address? return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false; } //Is it a syntactically valid hostname (when embeded in a URL)? return filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false; } /** * Get an error message in the current language. * * @param string $key * * @return string */ protected function lang($key) { if (count($this->language) < 1) { $this->setLanguage(); //Set the default language } if (array_key_exists($key, $this->language)) { if ('smtp_connect_failed' === $key) { //Include a link to troubleshooting docs on SMTP connection failure. //This is by far the biggest cause of support questions //but it's usually not PHPMailer's fault. return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; } return $this->language[$key]; } //Return the key as a fallback return $key; } /** * Build an error message starting with a generic one and adding details if possible. * * @param string $base_key * @return string */ private function getSmtpErrorMessage($base_key) { $message = $this->lang($base_key); $error = $this->smtp->getError(); if (!empty($error['error'])) { $message .= ' ' . $error['error']; if (!empty($error['detail'])) { $message .= ' ' . $error['detail']; } } return $message; } /** * Check if an error occurred. * * @return bool True if an error did occur */ public function isError() { return $this->error_count > 0; } /** * Add a custom header. * $name value can be overloaded to contain * both header name and value (name:value). * * @param string $name Custom header name * @param string|null $value Header value * * @throws Exception */ public function addCustomHeader($name, $value = null) { if (null === $value && strpos($name, ':') !== false) { //Value passed in as name:value list($name, $value) = explode(':', $name, 2); } $name = trim($name); $value = (null === $value) ? '' : trim($value); //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { throw new Exception($this->lang('invalid_header')); } return false; } $this->CustomHeader[] = [$name, $value]; return true; } /** * Returns all custom headers. * * @return array */ public function getCustomHeaders() { return $this->CustomHeader; } /** * Create a message body from an HTML string. * Automatically inlines images and creates a plain-text version by converting the HTML, * overwriting any existing values in Body and AltBody. * Do not source $message content from user input! * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty * will look for an image file in $basedir/images/a.png and convert it to inline. * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) * Converts data-uri images into embedded attachments. * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. * * @param string $message HTML message string * @param string $basedir Absolute path to a base directory to prepend to relative paths to images * @param bool|callable $advanced Whether to use the internal HTML to text converter * or your own custom converter * @return string The transformed message body * * @throws Exception * * @see PHPMailer::html2text() */ public function msgHTML($message, $basedir = '', $advanced = false) { preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images); if (array_key_exists(2, $images)) { if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { //Ensure $basedir has a trailing / $basedir .= '/'; } foreach ($images[2] as $imgindex => $url) { //Convert data URIs into embedded images //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" $match = []; if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) { if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) { $data = base64_decode($match[3]); } elseif ('' === $match[2]) { $data = rawurldecode($match[3]); } else { //Not recognised so leave it alone continue; } //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places //will only be embedded once, even if it used a different encoding $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2 if (!$this->cidExists($cid)) { $this->addStringEmbeddedImage( $data, $cid, 'embed' . $imgindex, static::ENCODING_BASE64, $match[1] ); } $message = str_replace( $images[0][$imgindex], $images[1][$imgindex] . '="cid:' . $cid . '"', $message ); continue; } if ( //Only process relative URLs if a basedir is provided (i.e. no absolute local paths) !empty($basedir) //Ignore URLs containing parent dir traversal (..) && (strpos($url, '..') === false) //Do not change urls that are already inline images && 0 !== strpos($url, 'cid:') //Do not change absolute URLs, including anonymous protocol && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) ) { $filename = static::mb_pathinfo($url, PATHINFO_BASENAME); $directory = dirname($url); if ('.' === $directory) { $directory = ''; } //RFC2392 S 2 $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0'; if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { $basedir .= '/'; } if (strlen($directory) > 1 && '/' !== substr($directory, -1)) { $directory .= '/'; } if ( $this->addEmbeddedImage( $basedir . $directory . $filename, $cid, $filename, static::ENCODING_BASE64, static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) ) ) { $message = preg_replace( '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', $images[1][$imgindex] . '="cid:' . $cid . '"', $message ); } } } } $this->isHTML(); //Convert all message body line breaks to LE, makes quoted-printable encoding work much better $this->Body = static::normalizeBreaks($message); $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced)); if (!$this->alternativeExists()) { $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' . static::$LE; } return $this->Body; } /** * Convert an HTML string into plain text. * This is used by msgHTML(). * Note - older versions of this function used a bundled advanced converter * which was removed for license reasons in #232. * Example usage: * * ```php * //Use default conversion * $plain = $mail->html2text($html); * //Use your own custom converter * $plain = $mail->html2text($html, function($html) { * $converter = new MyHtml2text($html); * return $converter->get_text(); * }); * ``` * * @param string $html The HTML text to convert * @param bool|callable $advanced Any boolean value to use the internal converter, * or provide your own callable for custom conversion. * *Never* pass user-supplied data into this parameter * * @return string */ public function html2text($html, $advanced = false) { if (is_callable($advanced)) { return call_user_func($advanced, $html); } return html_entity_decode( trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), ENT_QUOTES, $this->CharSet ); } /** * Get the MIME type for a file extension. * * @param string $ext File extension * * @return string MIME type of file */ public static function _mime_types($ext = '') { $mimes = [ 'xl' => 'application/excel', 'js' => 'application/javascript', 'hqx' => 'application/mac-binhex40', 'cpt' => 'application/mac-compactpro', 'bin' => 'application/macbinary', 'doc' => 'application/msword', 'word' => 'application/msword', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'class' => 'application/octet-stream', 'dll' => 'application/octet-stream', 'dms' => 'application/octet-stream', 'exe' => 'application/octet-stream', 'lha' => 'application/octet-stream', 'lzh' => 'application/octet-stream', 'psd' => 'application/octet-stream', 'sea' => 'application/octet-stream', 'so' => 'application/octet-stream', 'oda' => 'application/oda', 'pdf' => 'application/pdf', 'ai' => 'application/postscript', 'eps' => 'application/postscript', 'ps' => 'application/postscript', 'smi' => 'application/smil', 'smil' => 'application/smil', 'mif' => 'application/vnd.mif', 'xls' => 'application/vnd.ms-excel', 'ppt' => 'application/vnd.ms-powerpoint', 'wbxml' => 'application/vnd.wap.wbxml', 'wmlc' => 'application/vnd.wap.wmlc', 'dcr' => 'application/x-director', 'dir' => 'application/x-director', 'dxr' => 'application/x-director', 'dvi' => 'application/x-dvi', 'gtar' => 'application/x-gtar', 'php3' => 'application/x-httpd-php', 'php4' => 'application/x-httpd-php', 'php' => 'application/x-httpd-php', 'phtml' => 'application/x-httpd-php', 'phps' => 'application/x-httpd-php-source', 'swf' => 'application/x-shockwave-flash', 'sit' => 'application/x-stuffit', 'tar' => 'application/x-tar', 'tgz' => 'application/x-tar', 'xht' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml', 'zip' => 'application/zip', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mp2' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'mpga' => 'audio/mpeg', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'ram' => 'audio/x-pn-realaudio', 'rm' => 'audio/x-pn-realaudio', 'rpm' => 'audio/x-pn-realaudio-plugin', 'ra' => 'audio/x-realaudio', 'wav' => 'audio/x-wav', 'mka' => 'audio/x-matroska', 'bmp' => 'image/bmp', 'gif' => 'image/gif', 'jpeg' => 'image/jpeg', 'jpe' => 'image/jpeg', 'jpg' => 'image/jpeg', 'png' => 'image/png', 'tiff' => 'image/tiff', 'tif' => 'image/tiff', 'webp' => 'image/webp', 'avif' => 'image/avif', 'heif' => 'image/heif', 'heifs' => 'image/heif-sequence', 'heic' => 'image/heic', 'heics' => 'image/heic-sequence', 'eml' => 'message/rfc822', 'css' => 'text/css', 'html' => 'text/html', 'htm' => 'text/html', 'shtml' => 'text/html', 'log' => 'text/plain', 'text' => 'text/plain', 'txt' => 'text/plain', 'rtx' => 'text/richtext', 'rtf' => 'text/rtf', 'vcf' => 'text/vcard', 'vcard' => 'text/vcard', 'ics' => 'text/calendar', 'xml' => 'text/xml', 'xsl' => 'text/xml', 'wmv' => 'video/x-ms-wmv', 'mpeg' => 'video/mpeg', 'mpe' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'mov' => 'video/quicktime', 'qt' => 'video/quicktime', 'rv' => 'video/vnd.rn-realvideo', 'avi' => 'video/x-msvideo', 'movie' => 'video/x-sgi-movie', 'webm' => 'video/webm', 'mkv' => 'video/x-matroska', ]; $ext = strtolower($ext); if (array_key_exists($ext, $mimes)) { return $mimes[$ext]; } return 'application/octet-stream'; } /** * Map a file name to a MIME type. * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. * * @param string $filename A file name or full path, does not need to exist as a file * * @return string */ public static function filenameToType($filename) { //In case the path is a URL, strip any query string before getting extension $qpos = strpos($filename, '?'); if (false !== $qpos) { $filename = substr($filename, 0, $qpos); } $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); return static::_mime_types($ext); } /** * Multi-byte-safe pathinfo replacement. * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe. * * @see http://www.php.net/manual/en/function.pathinfo.php#107461 * * @param string $path A filename or path, does not need to exist as a file * @param int|string $options Either a PATHINFO_* constant, * or a string name to return only the specified piece * * @return string|array */ public static function mb_pathinfo($path, $options = null) { $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; $pathinfo = []; if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) { if (array_key_exists(1, $pathinfo)) { $ret['dirname'] = $pathinfo[1]; } if (array_key_exists(2, $pathinfo)) { $ret['basename'] = $pathinfo[2]; } if (array_key_exists(5, $pathinfo)) { $ret['extension'] = $pathinfo[5]; } if (array_key_exists(3, $pathinfo)) { $ret['filename'] = $pathinfo[3]; } } switch ($options) { case PATHINFO_DIRNAME: case 'dirname': return $ret['dirname']; case PATHINFO_BASENAME: case 'basename': return $ret['basename']; case PATHINFO_EXTENSION: case 'extension': return $ret['extension']; case PATHINFO_FILENAME: case 'filename': return $ret['filename']; default: return $ret; } } /** * Set or reset instance properties. * You should avoid this function - it's more verbose, less efficient, more error-prone and * harder to debug than setting properties directly. * Usage Example: * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);` * is the same as: * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`. * * @param string $name The property name to set * @param mixed $value The value to set the property to * * @return bool */ public function set($name, $value = '') { if (property_exists($this, $name)) { $this->{$name} = $value; return true; } $this->setError($this->lang('variable_set') . $name); return false; } /** * Strip newlines to prevent header injection. * * @param string $str * * @return string */ public function secureHeader($str) { return trim(str_replace(["\r", "\n"], '', $str)); } /** * Normalize line breaks in a string. * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. * Defaults to CRLF (for message bodies) and preserves consecutive breaks. * * @param string $text * @param string $breaktype What kind of line break to use; defaults to static::$LE * * @return string */ public static function normalizeBreaks($text, $breaktype = null) { if (null === $breaktype) { $breaktype = static::$LE; } //Normalise to \n $text = str_replace([self::CRLF, "\r"], "\n", $text); //Now convert LE as needed if ("\n" !== $breaktype) { $text = str_replace("\n", $breaktype, $text); } return $text; } /** * Remove trailing breaks from a string. * * @param string $text * * @return string The text to remove breaks from */ public static function stripTrailingWSP($text) { return rtrim($text, " \r\n\t"); } /** * Return the current line break format string. * * @return string */ public static function getLE() { return static::$LE; } /** * Set the line break format string, e.g. "\r\n". * * @param string $le */ protected static function setLE($le) { static::$LE = $le; } /** * Set the public and private key files and password for S/MIME signing. * * @param string $cert_filename * @param string $key_filename * @param string $key_pass Password for private key * @param string $extracerts_filename Optional path to chain certificate */ public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') { $this->sign_cert_file = $cert_filename; $this->sign_key_file = $key_filename; $this->sign_key_pass = $key_pass; $this->sign_extracerts_file = $extracerts_filename; } /** * Quoted-Printable-encode a DKIM header. * * @param string $txt * * @return string */ public function DKIM_QP($txt) { $line = ''; $len = strlen($txt); for ($i = 0; $i < $len; ++$i) { $ord = ord($txt[$i]); if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) { $line .= $txt[$i]; } else { $line .= '=' . sprintf('%02X', $ord); } } return $line; } /** * Generate a DKIM signature. * * @param string $signHeader * * @throws Exception * * @return string The DKIM signature value */ public function DKIM_Sign($signHeader) { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { throw new Exception($this->lang('extension_missing') . 'openssl'); } return ''; } $privKeyStr = !empty($this->DKIM_private_string) ? $this->DKIM_private_string : file_get_contents($this->DKIM_private); if ('' !== $this->DKIM_passphrase) { $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); } else { $privKey = openssl_pkey_get_private($privKeyStr); } if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { if (\PHP_MAJOR_VERSION < 8) { openssl_pkey_free($privKey); } return base64_encode($signature); } if (\PHP_MAJOR_VERSION < 8) { openssl_pkey_free($privKey); } return ''; } /** * Generate a DKIM canonicalization header. * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. * Canonicalized headers should *always* use CRLF, regardless of mailer setting. * * @see https://tools.ietf.org/html/rfc6376#section-3.4.2 * * @param string $signHeader Header * * @return string */ public function DKIM_HeaderC($signHeader) { //Normalize breaks to CRLF (regardless of the mailer) $signHeader = static::normalizeBreaks($signHeader, self::CRLF); //Unfold header lines //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` //@see https://tools.ietf.org/html/rfc5322#section-2.2 //That means this may break if you do something daft like put vertical tabs in your headers. $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); //Break headers out into an array $lines = explode(self::CRLF, $signHeader); foreach ($lines as $key => $line) { //If the header is missing a :, skip it as it's invalid //This is likely to happen because the explode() above will also split //on the trailing LE, leaving an empty line if (strpos($line, ':') === false) { continue; } list($heading, $value) = explode(':', $line, 2); //Lower-case header name $heading = strtolower($heading); //Collapse white space within the value, also convert WSP to space $value = preg_replace('/[ \t]+/', ' ', $value); //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value //But then says to delete space before and after the colon. //Net result is the same as trimming both ends of the value. //By elimination, the same applies to the field name $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t"); } return implode(self::CRLF, $lines); } /** * Generate a DKIM canonicalization body. * Uses the 'simple' algorithm from RFC6376 section 3.4.3. * Canonicalized bodies should *always* use CRLF, regardless of mailer setting. * * @see https://tools.ietf.org/html/rfc6376#section-3.4.3 * * @param string $body Message Body * * @return string */ public function DKIM_BodyC($body) { if (empty($body)) { return self::CRLF; } //Normalize line endings to CRLF $body = static::normalizeBreaks($body, self::CRLF); //Reduce multiple trailing line breaks to a single one return static::stripTrailingWSP($body) . self::CRLF; } /** * Create the DKIM header and body in a new message header. * * @param string $headers_line Header lines * @param string $subject Subject * @param string $body Body * * @throws Exception * * @return string */ public function DKIM_Add($headers_line, $subject, $body) { $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body $DKIMquery = 'dns/txt'; //Query method $DKIMtime = time(); //Always sign these headers without being asked //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1 $autoSignHeaders = [ 'from', 'to', 'cc', 'date', 'subject', 'reply-to', 'message-id', 'content-type', 'mime-version', 'x-mailer', ]; if (stripos($headers_line, 'Subject') === false) { $headers_line .= 'Subject: ' . $subject . static::$LE; } $headerLines = explode(static::$LE, $headers_line); $currentHeaderLabel = ''; $currentHeaderValue = ''; $parsedHeaders = []; $headerLineIndex = 0; $headerLineCount = count($headerLines); foreach ($headerLines as $headerLine) { $matches = []; if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) { if ($currentHeaderLabel !== '') { //We were previously in another header; This is the start of a new header, so save the previous one $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue]; } $currentHeaderLabel = $matches[1]; $currentHeaderValue = $matches[2]; } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) { //This is a folded continuation of the current header, so unfold it $currentHeaderValue .= ' ' . $matches[1]; } ++$headerLineIndex; if ($headerLineIndex >= $headerLineCount) { //This was the last line, so finish off this header $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue]; } } $copiedHeaders = []; $headersToSignKeys = []; $headersToSign = []; foreach ($parsedHeaders as $header) { //Is this header one that must be included in the DKIM signature? if (in_array(strtolower($header['label']), $autoSignHeaders, true)) { $headersToSignKeys[] = $header['label']; $headersToSign[] = $header['label'] . ': ' . $header['value']; if ($this->DKIM_copyHeaderFields) { $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC str_replace('|', '=7C', $this->DKIM_QP($header['value'])); } continue; } //Is this an extra custom header we've been asked to sign? if (in_array($header['label'], $this->DKIM_extraHeaders, true)) { //Find its value in custom headers foreach ($this->CustomHeader as $customHeader) { if ($customHeader[0] === $header['label']) { $headersToSignKeys[] = $header['label']; $headersToSign[] = $header['label'] . ': ' . $header['value']; if ($this->DKIM_copyHeaderFields) { $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC str_replace('|', '=7C', $this->DKIM_QP($header['value'])); } //Skip straight to the next header continue 2; } } } } $copiedHeaderFields = ''; if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) { //Assemble a DKIM 'z' tag $copiedHeaderFields = ' z='; $first = true; foreach ($copiedHeaders as $copiedHeader) { if (!$first) { $copiedHeaderFields .= static::$LE . ' |'; } //Fold long values if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) { $copiedHeaderFields .= substr( chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS), 0, -strlen(static::$LE . self::FWS) ); } else { $copiedHeaderFields .= $copiedHeader; } $first = false; } $copiedHeaderFields .= ';' . static::$LE; } $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE; $headerValues = implode(static::$LE, $headersToSign); $body = $this->DKIM_BodyC($body); //Base64 of packed binary SHA-256 hash of body $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); $ident = ''; if ('' !== $this->DKIM_identity) { $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE; } //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag //which is appended after calculating the signature //https://tools.ietf.org/html/rfc6376#section-3.5 $dkimSignatureHeader = 'DKIM-Signature: v=1;' . ' d=' . $this->DKIM_domain . ';' . ' s=' . $this->DKIM_selector . ';' . static::$LE . ' a=' . $DKIMsignatureType . ';' . ' q=' . $DKIMquery . ';' . ' t=' . $DKIMtime . ';' . ' c=' . $DKIMcanonicalization . ';' . static::$LE . $headerKeys . $ident . $copiedHeaderFields . ' bh=' . $DKIMb64 . ';' . static::$LE . ' b='; //Canonicalize the set of headers $canonicalizedHeaders = $this->DKIM_HeaderC( $headerValues . static::$LE . $dkimSignatureHeader ); $signature = $this->DKIM_Sign($canonicalizedHeaders); $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS)); return static::normalizeBreaks($dkimSignatureHeader . $signature); } /** * Detect if a string contains a line longer than the maximum line length * allowed by RFC 2822 section 2.1.1. * * @param string $str * * @return bool */ public static function hasLineLongerThanMax($str) { return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str); } /** * If a string contains any "special" characters, double-quote the name, * and escape any double quotes with a backslash. * * @param string $str * * @return string * * @see RFC822 3.4.1 */ public static function quotedString($str) { if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) { //If the string contains any of these chars, it must be double-quoted //and any double quotes must be escaped with a backslash return '"' . str_replace('"', '\\"', $str) . '"'; } //Return the string untouched, it doesn't need quoting return $str; } /** * Allows for public read access to 'to' property. * Before the send() call, queued addresses (i.e. with IDN) are not yet included. * * @return array */ public function getToAddresses() { return $this->to; } /** * Allows for public read access to 'cc' property. * Before the send() call, queued addresses (i.e. with IDN) are not yet included. * * @return array */ public function getCcAddresses() { return $this->cc; } /** * Allows for public read access to 'bcc' property. * Before the send() call, queued addresses (i.e. with IDN) are not yet included. * * @return array */ public function getBccAddresses() { return $this->bcc; } /** * Allows for public read access to 'ReplyTo' property. * Before the send() call, queued addresses (i.e. with IDN) are not yet included. * * @return array */ public function getReplyToAddresses() { return $this->ReplyTo; } /** * Allows for public read access to 'all_recipients' property. * Before the send() call, queued addresses (i.e. with IDN) are not yet included. * * @return array */ public function getAllRecipientAddresses() { return $this->all_recipients; } /** * Perform a callback. * * @param bool $isSent * @param array $to * @param array $cc * @param array $bcc * @param string $subject * @param string $body * @param string $from * @param array $extra */ protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra) { if (!empty($this->action_function) && is_callable($this->action_function)) { call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra); } } /** * Get the OAuthTokenProvider instance. * * @return OAuthTokenProvider */ public function getOAuth() { return $this->oauth; } /** * Set an OAuthTokenProvider instance. */ public function setOAuth(OAuthTokenProvider $oauth) { $this->oauth = $oauth; } } Exception.php 0000644 00000002330 15025064760 0007216 0 ustar 00 <?php /** * PHPMailer Exception class. * PHP Version 5.5. * * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk> * @author Jim Jagielski (jimjag) <jimjag@gmail.com> * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ namespace PHPMailer\PHPMailer; /** * PHPMailer exception handler. * * @author Marcus Bointon <phpmailer@synchromedia.co.uk> */ class Exception extends \Exception { /** * Prettify error message output. * * @return string */ public function errorMessage() { return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n"; } }
| ver. 1.4 |
.
| PHP 8.0.30 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings