Iterators/Mapper.php000064400000001131150250563100010446 0ustar00callback = $callback; } public function current(): mixed { return ($this->callback)(parent::current(), parent::key()); } } Iterators/CachingIterator.php000064400000005762150250563100012306 0ustar00getIterator(); } while ($iterator instanceof \IteratorAggregate); assert($iterator instanceof \Iterator); } elseif ($iterator instanceof \Iterator) { } elseif ($iterator instanceof \Traversable) { $iterator = new \IteratorIterator($iterator); } else { throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, is_object($iterator) ? $iterator::class : gettype($iterator))); } parent::__construct($iterator, 0); } /** * Is the current element the first one? */ public function isFirst(?int $gridWidth = null): bool { return $this->counter === 1 || ($gridWidth && $this->counter !== 0 && (($this->counter - 1) % $gridWidth) === 0); } /** * Is the current element the last one? */ public function isLast(?int $gridWidth = null): bool { return !$this->hasNext() || ($gridWidth && ($this->counter % $gridWidth) === 0); } /** * Is the iterator empty? */ public function isEmpty(): bool { return $this->counter === 0; } /** * Is the counter odd? */ public function isOdd(): bool { return $this->counter % 2 === 1; } /** * Is the counter even? */ public function isEven(): bool { return $this->counter % 2 === 0; } /** * Returns the counter. */ public function getCounter(): int { return $this->counter; } /** * Returns the count of elements. */ public function count(): int { $inner = $this->getInnerIterator(); if ($inner instanceof \Countable) { return $inner->count(); } else { throw new Nette\NotSupportedException('Iterator is not countable.'); } } /** * Forwards to the next element. */ public function next(): void { parent::next(); if (parent::valid()) { $this->counter++; } } /** * Rewinds the Iterator. */ public function rewind(): void { parent::rewind(); $this->counter = parent::valid() ? 1 : 0; } /** * Returns the next key. */ public function getNextKey(): mixed { return $this->getInnerIterator()->key(); } /** * Returns the next element. */ public function getNextValue(): mixed { return $this->getInnerIterator()->current(); } } Translator.php000064400000000654150250563100007410 0ustar00$name ?? null; if (is_iterable($handlers)) { foreach ($handlers as $handler) { $handler(...$args); } } elseif ($handlers !== null) { throw new UnexpectedValueException("Property $class::$$name must be iterable or null, " . gettype($handlers) . ' given.'); } return null; } ObjectHelpers::strictCall($class, $name); } /** * @throws MemberAccessException */ public static function __callStatic(string $name, array $args) { ObjectHelpers::strictStaticCall(static::class, $name); } /** * @return mixed * @throws MemberAccessException if the property is not defined. */ public function &__get(string $name) { $class = static::class; if ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property getter if (!($prop & 0b0001)) { throw new MemberAccessException("Cannot read a write-only property $class::\$$name."); } $m = ($prop & 0b0010 ? 'get' : 'is') . ucfirst($name); if ($prop & 0b10000) { $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() $loc = isset($trace['file'], $trace['line']) ? " in $trace[file] on line $trace[line]" : ''; trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); } if ($prop & 0b0100) { // return by reference return $this->$m(); } else { $val = $this->$m(); return $val; } } else { ObjectHelpers::strictGet($class, $name); } } /** * @throws MemberAccessException if the property is not defined or is read-only */ public function __set(string $name, mixed $value): void { $class = static::class; if (ObjectHelpers::hasProperty($class, $name)) { // unsetted property $this->$name = $value; } elseif ($prop = ObjectHelpers::getMagicProperties($class)[$name] ?? null) { // property setter if (!($prop & 0b1000)) { throw new MemberAccessException("Cannot write to a read-only property $class::\$$name."); } $m = 'set' . ucfirst($name); if ($prop & 0b10000) { $trace = debug_backtrace(0, 1)[0]; // suppose this method is called from __call() $loc = isset($trace['file'], $trace['line']) ? " in $trace[file] on line $trace[line]" : ''; trigger_error("Property $class::\$$name is deprecated, use $class::$m() method$loc.", E_USER_DEPRECATED); } $this->$m($value); } else { ObjectHelpers::strictSet($class, $name); } } /** * @throws MemberAccessException */ public function __unset(string $name): void { $class = static::class; if (!ObjectHelpers::hasProperty($class, $name)) { throw new MemberAccessException("Cannot unset the property $class::\$$name."); } } public function __isset(string $name): bool { return isset(ObjectHelpers::getMagicProperties(static::class)[$name]); } } compatibility.php000064400000001256150250563100010127 0ustar00 $b it returns 1 * @throws \LogicException if one of parameters is NAN */ public static function compare(float $a, float $b): int { if (is_nan($a) || is_nan($b)) { throw new \LogicException('Trying to compare NAN'); } elseif (!is_finite($a) && !is_finite($b) && $a === $b) { return 0; } $diff = abs($a - $b); if (($diff < self::Epsilon || ($diff / max(abs($a), abs($b)) < self::Epsilon))) { return 0; } return $a < $b ? -1 : 1; } /** * Returns true if $a = $b * @throws \LogicException if one of parameters is NAN */ public static function areEqual(float $a, float $b): bool { return self::compare($a, $b) === 0; } /** * Returns true if $a < $b * @throws \LogicException if one of parameters is NAN */ public static function isLessThan(float $a, float $b): bool { return self::compare($a, $b) < 0; } /** * Returns true if $a <= $b * @throws \LogicException if one of parameters is NAN */ public static function isLessThanOrEqualTo(float $a, float $b): bool { return self::compare($a, $b) <= 0; } /** * Returns true if $a > $b * @throws \LogicException if one of parameters is NAN */ public static function isGreaterThan(float $a, float $b): bool { return self::compare($a, $b) > 0; } /** * Returns true if $a >= $b * @throws \LogicException if one of parameters is NAN */ public static function isGreaterThanOrEqualTo(float $a, float $b): bool { return self::compare($a, $b) >= 0; } } Utils/Image.php000064400000052425150250563100007404 0ustar00 * $image = Image::fromFile('nette.jpg'); * $image->resize(150, 100); * $image->sharpen(); * $image->send(); * * * @method Image affine(array $affine, array $clip = null) * @method array affineMatrixConcat(array $m1, array $m2) * @method array affineMatrixGet(int $type, mixed $options = null) * @method void alphaBlending(bool $on) * @method void antialias(bool $on) * @method void arc($x, $y, $w, $h, $start, $end, $color) * @method void char(int $font, $x, $y, string $char, $color) * @method void charUp(int $font, $x, $y, string $char, $color) * @method int colorAllocate($red, $green, $blue) * @method int colorAllocateAlpha($red, $green, $blue, $alpha) * @method int colorAt($x, $y) * @method int colorClosest($red, $green, $blue) * @method int colorClosestAlpha($red, $green, $blue, $alpha) * @method int colorClosestHWB($red, $green, $blue) * @method void colorDeallocate($color) * @method int colorExact($red, $green, $blue) * @method int colorExactAlpha($red, $green, $blue, $alpha) * @method void colorMatch(Image $image2) * @method int colorResolve($red, $green, $blue) * @method int colorResolveAlpha($red, $green, $blue, $alpha) * @method void colorSet($index, $red, $green, $blue) * @method array colorsForIndex($index) * @method int colorsTotal() * @method int colorTransparent($color = null) * @method void convolution(array $matrix, float $div, float $offset) * @method void copy(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH) * @method void copyMerge(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyMergeGray(Image $src, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $opacity) * @method void copyResampled(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method void copyResized(Image $src, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH) * @method Image cropAuto(int $mode = -1, float $threshold = .5, int $color = -1) * @method void ellipse($cx, $cy, $w, $h, $color) * @method void fill($x, $y, $color) * @method void filledArc($cx, $cy, $w, $h, $s, $e, $color, $style) * @method void filledEllipse($cx, $cy, $w, $h, $color) * @method void filledPolygon(array $points, $numPoints, $color) * @method void filledRectangle($x1, $y1, $x2, $y2, $color) * @method void fillToBorder($x, $y, $border, $color) * @method void filter($filtertype) * @method void flip(int $mode) * @method array ftText($size, $angle, $x, $y, $col, string $fontFile, string $text, array $extrainfo = null) * @method void gammaCorrect(float $inputgamma, float $outputgamma) * @method array getClip() * @method int interlace($interlace = null) * @method bool isTrueColor() * @method void layerEffect($effect) * @method void line($x1, $y1, $x2, $y2, $color) * @method void openPolygon(array $points, int $num_points, int $color) * @method void paletteCopy(Image $source) * @method void paletteToTrueColor() * @method void polygon(array $points, $numPoints, $color) * @method array psText(string $text, $font, $size, $color, $backgroundColor, $x, $y, $space = null, $tightness = null, float $angle = null, $antialiasSteps = null) * @method void rectangle($x1, $y1, $x2, $y2, $col) * @method mixed resolution(int $res_x = null, int $res_y = null) * @method Image rotate(float $angle, $backgroundColor) * @method void saveAlpha(bool $saveflag) * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) * @method void setBrush(Image $brush) * @method void setClip(int $x1, int $y1, int $x2, int $y2) * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) * @method void setPixel($x, $y, $color) * @method void setStyle(array $style) * @method void setThickness($thickness) * @method void setTile(Image $tile) * @method void string($font, $x, $y, string $s, $col) * @method void stringUp($font, $x, $y, string $s, $col) * @method void trueColorToPalette(bool $dither, $ncolors) * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text) * @property-read int $width * @property-read int $height * @property-read \GdImage $imageResource */ class Image { use Nette\SmartObject; /** Prevent from getting resized to a bigger size than the original */ public const ShrinkOnly = 0b0001; /** Resizes to a specified width and height without keeping aspect ratio */ public const Stretch = 0b0010; /** Resizes to fit into a specified width and height and preserves aspect ratio */ public const OrSmaller = 0b0000; /** Resizes while bounding the smaller dimension to the specified width or height and preserves aspect ratio */ public const OrBigger = 0b0100; /** Resizes to the smallest possible size to completely cover specified width and height and reserves aspect ratio */ public const Cover = 0b1000; /** @deprecated use Image::ShrinkOnly */ public const SHRINK_ONLY = self::ShrinkOnly; /** @deprecated use Image::Stretch */ public const STRETCH = self::Stretch; /** @deprecated use Image::OrSmaller */ public const FIT = self::OrSmaller; /** @deprecated use Image::OrBigger */ public const FILL = self::OrBigger; /** @deprecated use Image::Cover */ public const EXACT = self::Cover; /** @deprecated use Image::EmptyGIF */ public const EMPTY_GIF = self::EmptyGIF; /** image types */ public const JPEG = IMAGETYPE_JPEG, PNG = IMAGETYPE_PNG, GIF = IMAGETYPE_GIF, WEBP = IMAGETYPE_WEBP, AVIF = 19, // IMAGETYPE_AVIF, BMP = IMAGETYPE_BMP; public const EmptyGIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"; private const Formats = [self::JPEG => 'jpeg', self::PNG => 'png', self::GIF => 'gif', self::WEBP => 'webp', self::AVIF => 'avif', self::BMP => 'bmp']; private \GdImage $image; /** * Returns RGB color (0..255) and transparency (0..127). */ public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array { return [ 'red' => max(0, min(255, $red)), 'green' => max(0, min(255, $green)), 'blue' => max(0, min(255, $blue)), 'alpha' => max(0, min(127, $transparency)), ]; } /** * Reads an image from a file and returns its type in $type. * @throws Nette\NotSupportedException if gd extension is not loaded * @throws UnknownImageFileException if file not found or file type is not known */ public static function fromFile(string $file, ?int &$type = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } $type = self::detectTypeFromFile($file); if (!$type) { throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found."); } return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__); } /** * Reads an image from a string and returns its type in $type. * @throws Nette\NotSupportedException if gd extension is not loaded * @throws ImageException */ public static function fromString(string $s, ?int &$type = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } $type = self::detectTypeFromString($s); if (!$type) { throw new UnknownImageFileException('Unknown type of image.'); } return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__); } private static function invokeSafe(string $func, string $arg, string $message, string $callee): static { $errors = []; $res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void { $errors[] = $message; }); if (!$res) { throw new ImageException($message . ' Errors: ' . implode(', ', $errors)); } elseif ($errors) { trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING); } return new static($res); } /** * Creates a new true color image of the given dimensions. The default color is black. * @throws Nette\NotSupportedException if gd extension is not loaded */ public static function fromBlank(int $width, int $height, ?array $color = null): static { if (!extension_loaded('gd')) { throw new Nette\NotSupportedException('PHP extension GD is not loaded.'); } if ($width < 1 || $height < 1) { throw new Nette\InvalidArgumentException('Image width and height must be greater than zero.'); } $image = imagecreatetruecolor($width, $height); if ($color) { $color += ['alpha' => 0]; $color = imagecolorresolvealpha($image, $color['red'], $color['green'], $color['blue'], $color['alpha']); imagealphablending($image, false); imagefilledrectangle($image, 0, 0, $width - 1, $height - 1, $color); imagealphablending($image, true); } return new static($image); } /** * Returns the type of image from file. */ public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesize($file); // @ - files smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the type of image from string. */ public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int { [$width, $height, $type] = @getimagesizefromstring($s); // @ - strings smaller than 12 bytes causes read error return isset(self::Formats[$type]) ? $type : null; } /** * Returns the file extension for the given `Image::XXX` constant. */ public static function typeToExtension(int $type): string { if (!isset(self::Formats[$type])) { throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } return self::Formats[$type]; } /** * Returns the `Image::XXX` constant for given file extension. */ public static function extensionToType(string $extension): int { $extensions = array_flip(self::Formats) + ['jpg' => self::JPEG]; $extension = strtolower($extension); if (!isset($extensions[$extension])) { throw new Nette\InvalidArgumentException("Unsupported file extension '$extension'."); } return $extensions[$extension]; } /** * Returns the mime type for the given `Image::XXX` constant. */ public static function typeToMimeType(int $type): string { return 'image/' . self::typeToExtension($type); } /** * Wraps GD image. */ public function __construct(\GdImage $image) { $this->setImageResource($image); imagesavealpha($image, true); } /** * Returns image width. */ public function getWidth(): int { return imagesx($this->image); } /** * Returns image height. */ public function getHeight(): int { return imagesy($this->image); } /** * Sets image resource. */ protected function setImageResource(\GdImage $image): static { $this->image = $image; return $this; } /** * Returns image GD resource. */ public function getImageResource(): \GdImage { return $this->image; } /** * Scales an image. Width and height accept pixels or percent. * @param self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly $mode */ public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static { if ($mode & self::Cover) { return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height); } [$newWidth, $newHeight] = static::calculateSize($this->getWidth(), $this->getHeight(), $width, $height, $mode); if ($newWidth !== $this->getWidth() || $newHeight !== $this->getHeight()) { // resize $newImage = static::fromBlank($newWidth, $newHeight, self::rgb(0, 0, 0, 127))->getImageResource(); imagecopyresampled( $newImage, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->getWidth(), $this->getHeight(), ); $this->image = $newImage; } if ($width < 0 || $height < 0) { imageflip($this->image, $width < 0 ? ($height < 0 ? IMG_FLIP_BOTH : IMG_FLIP_HORIZONTAL) : IMG_FLIP_VERTICAL); } return $this; } /** * Calculates dimensions of resized image. Width and height accept pixels or percent. * @param self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly $mode */ public static function calculateSize( int $srcWidth, int $srcHeight, $newWidth, $newHeight, int $mode = self::OrSmaller, ): array { if ($newWidth === null) { } elseif (self::isPercent($newWidth)) { $newWidth = (int) round($srcWidth / 100 * abs($newWidth)); $percents = true; } else { $newWidth = abs($newWidth); } if ($newHeight === null) { } elseif (self::isPercent($newHeight)) { $newHeight = (int) round($srcHeight / 100 * abs($newHeight)); $mode |= empty($percents) ? 0 : self::Stretch; } else { $newHeight = abs($newHeight); } if ($mode & self::Stretch) { // non-proportional if (!$newWidth || !$newHeight) { throw new Nette\InvalidArgumentException('For stretching must be both width and height specified.'); } if ($mode & self::ShrinkOnly) { $newWidth = min($srcWidth, $newWidth); $newHeight = min($srcHeight, $newHeight); } } else { // proportional if (!$newWidth && !$newHeight) { throw new Nette\InvalidArgumentException('At least width or height must be specified.'); } $scale = []; if ($newWidth > 0) { // fit width $scale[] = $newWidth / $srcWidth; } if ($newHeight > 0) { // fit height $scale[] = $newHeight / $srcHeight; } if ($mode & self::OrBigger) { $scale = [max($scale)]; } if ($mode & self::ShrinkOnly) { $scale[] = 1; } $scale = min($scale); $newWidth = (int) round($srcWidth * $scale); $newHeight = (int) round($srcHeight * $scale); } return [max($newWidth, 1), max($newHeight, 1)]; } /** * Crops image. Arguments accepts pixels or percent. */ public function crop(int|string $left, int|string $top, int|string $width, int|string $height): static { [$r['x'], $r['y'], $r['width'], $r['height']] = static::calculateCutout($this->getWidth(), $this->getHeight(), $left, $top, $width, $height); if (gd_info()['GD Version'] === 'bundled (2.1.0 compatible)') { $this->image = imagecrop($this->image, $r); imagesavealpha($this->image, true); } else { $newImage = static::fromBlank($r['width'], $r['height'], self::RGB(0, 0, 0, 127))->getImageResource(); imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']); $this->image = $newImage; } return $this; } /** * Calculates dimensions of cutout in image. Arguments accepts pixels or percent. */ public static function calculateCutout( int $srcWidth, int $srcHeight, int|string $left, int|string $top, int|string $newWidth, int|string $newHeight, ): array { if (self::isPercent($newWidth)) { $newWidth = (int) round($srcWidth / 100 * $newWidth); } if (self::isPercent($newHeight)) { $newHeight = (int) round($srcHeight / 100 * $newHeight); } if (self::isPercent($left)) { $left = (int) round(($srcWidth - $newWidth) / 100 * $left); } if (self::isPercent($top)) { $top = (int) round(($srcHeight - $newHeight) / 100 * $top); } if ($left < 0) { $newWidth += $left; $left = 0; } if ($top < 0) { $newHeight += $top; $top = 0; } $newWidth = min($newWidth, $srcWidth - $left); $newHeight = min($newHeight, $srcHeight - $top); return [$left, $top, $newWidth, $newHeight]; } /** * Sharpens image a little bit. */ public function sharpen(): static { imageconvolution($this->image, [ // my magic numbers ;) [-1, -1, -1], [-1, 24, -1], [-1, -1, -1], ], 16, 0); return $this; } /** * Puts another image into this image. Left and top accepts pixels or percent. * @param int $opacity 0..100 */ public function place(self $image, int|string $left = 0, int|string $top = 0, int $opacity = 100): static { $opacity = max(0, min(100, $opacity)); if ($opacity === 0) { return $this; } $width = $image->getWidth(); $height = $image->getHeight(); if (self::isPercent($left)) { $left = (int) round(($this->getWidth() - $width) / 100 * $left); } if (self::isPercent($top)) { $top = (int) round(($this->getHeight() - $height) / 100 * $top); } $output = $input = $image->image; if ($opacity < 100) { $tbl = []; for ($i = 0; $i < 128; $i++) { $tbl[$i] = round(127 - (127 - $i) * $opacity / 100); } $output = imagecreatetruecolor($width, $height); imagealphablending($output, false); if (!$image->isTrueColor()) { $input = $output; imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127)); imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height); } for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $c = \imagecolorat($input, $x, $y); $c = ($c & 0xFFFFFF) + ($tbl[$c >> 24] << 24); \imagesetpixel($output, $x, $y, $c); } } imagealphablending($output, true); } imagecopy( $this->image, $output, $left, $top, 0, 0, $width, $height, ); return $this; } /** * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * @throws ImageException */ public function save(string $file, ?int $quality = null, ?int $type = null): void { $type ??= self::extensionToType(pathinfo($file, PATHINFO_EXTENSION)); $this->output($type, $quality, $file); } /** * Outputs image to string. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). */ public function toString(int $type = self::JPEG, ?int $quality = null): string { return Helpers::capture(function () use ($type, $quality): void { $this->output($type, $quality); }); } /** * Outputs image to string. */ public function __toString(): string { return $this->toString(); } /** * Outputs image to browser. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). * @throws ImageException */ public function send(int $type = self::JPEG, ?int $quality = null): void { header('Content-Type: ' . self::typeToMimeType($type)); $this->output($type, $quality); } /** * Outputs image to browser or file. * @throws ImageException */ private function output(int $type, ?int $quality, ?string $file = null): void { switch ($type) { case self::JPEG: $quality = $quality === null ? 85 : max(0, min(100, $quality)); $success = @imagejpeg($this->image, $file, $quality); // @ is escalated to exception break; case self::PNG: $quality = $quality === null ? 9 : max(0, min(9, $quality)); $success = @imagepng($this->image, $file, $quality); // @ is escalated to exception break; case self::GIF: $success = @imagegif($this->image, $file); // @ is escalated to exception break; case self::WEBP: $quality = $quality === null ? 80 : max(0, min(100, $quality)); $success = @imagewebp($this->image, $file, $quality); // @ is escalated to exception break; case self::AVIF: $quality = $quality === null ? 30 : max(0, min(100, $quality)); $success = @imageavif($this->image, $file, $quality); // @ is escalated to exception break; case self::BMP: $success = @imagebmp($this->image, $file); // @ is escalated to exception break; default: throw new Nette\InvalidArgumentException("Unsupported image type '$type'."); } if (!$success) { throw new ImageException(Helpers::getLastError() ?: 'Unknown error'); } } /** * Call to undefined method. * @throws Nette\MemberAccessException */ public function __call(string $name, array $args): mixed { $function = 'image' . $name; if (!function_exists($function)) { ObjectHelpers::strictCall(static::class, $name); } foreach ($args as $key => $value) { if ($value instanceof self) { $args[$key] = $value->getImageResource(); } elseif (is_array($value) && isset($value['red'])) { // rgb $args[$key] = imagecolorallocatealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'], ) ?: imagecolorresolvealpha( $this->image, $value['red'], $value['green'], $value['blue'], $value['alpha'], ); } } $res = $function($this->image, ...$args); return $res instanceof \GdImage ? $this->setImageResource($res) : $res; } public function __clone() { ob_start(function () {}); imagepng($this->image, null, 0); $this->setImageResource(imagecreatefromstring(ob_get_clean())); } private static function isPercent(int|string &$num): bool { if (is_string($num) && str_ends_with($num, '%')) { $num = (float) substr($num, 0, -1); return true; } elseif (is_int($num) || $num === (string) (int) $num) { $num = (int) $num; return false; } throw new Nette\InvalidArgumentException("Expected dimension in int|string, '$num' given."); } /** * Prevents serialization. */ public function __sleep(): array { throw new Nette\NotSupportedException('You cannot serialize or unserialize ' . self::class . ' instances.'); } } Utils/Arrays.php000064400000025041150250563100007615 0ustar00 $array * @param array-key|array-key[] $key * @param ?T $default * @return ?T * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided */ public static function get(array $array, string|int|array $key, mixed $default = null): mixed { foreach (is_array($key) ? $key : [$key] as $k) { if (is_array($array) && array_key_exists($k, $array)) { $array = $array[$k]; } else { if (func_num_args() < 3) { throw new Nette\InvalidArgumentException("Missing item '$k'."); } return $default; } } return $array; } /** * Returns reference to array item. If the index does not exist, new one is created with value null. * @template T * @param array $array * @param array-key|array-key[] $key * @return ?T * @throws Nette\InvalidArgumentException if traversed item is not an array */ public static function &getRef(array &$array, string|int|array $key): mixed { foreach (is_array($key) ? $key : [$key] as $k) { if (is_array($array) || $array === null) { $array = &$array[$k]; } else { throw new Nette\InvalidArgumentException('Traversed item is not an array.'); } } return $array; } /** * Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as * the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains * the value from the first array in the case of a key collision. * @template T1 * @template T2 * @param array $array1 * @param array $array2 * @return array */ public static function mergeTree(array $array1, array $array2): array { $res = $array1 + $array2; foreach (array_intersect_key($array1, $array2) as $k => $v) { if (is_array($v) && is_array($array2[$k])) { $res[$k] = self::mergeTree($v, $array2[$k]); } } return $res; } /** * Returns zero-indexed position of given array key. Returns null if key is not found. */ public static function getKeyOffset(array $array, string|int $key): ?int { return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true)); } /** * @deprecated use getKeyOffset() */ public static function searchKey(array $array, $key): ?int { return self::getKeyOffset($array, $key); } /** * Tests an array for the presence of value. */ public static function contains(array $array, mixed $value): bool { return in_array($value, $array, true); } /** * Returns the first item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function first(array $array): mixed { return count($array) ? reset($array) : null; } /** * Returns the last item from the array or null if array is empty. * @template T * @param array $array * @return ?T */ public static function last(array $array): mixed { return count($array) ? end($array) : null; } /** * Inserts the contents of the $inserted array into the $array immediately after the $key. * If $key is null (or does not exist), it is inserted at the beginning. */ public static function insertBefore(array &$array, string|int|null $key, array $inserted): void { $offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key); $array = array_slice($array, 0, $offset, true) + $inserted + array_slice($array, $offset, count($array), true); } /** * Inserts the contents of the $inserted array into the $array before the $key. * If $key is null (or does not exist), it is inserted at the end. */ public static function insertAfter(array &$array, string|int|null $key, array $inserted): void { if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) { $offset = count($array) - 1; } $array = array_slice($array, 0, $offset + 1, true) + $inserted + array_slice($array, $offset + 1, count($array), true); } /** * Renames key in array. */ public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool { $offset = self::getKeyOffset($array, $oldKey); if ($offset === null) { return false; } $val = &$array[$oldKey]; $keys = array_keys($array); $keys[$offset] = $newKey; $array = array_combine($keys, $array); $array[$newKey] = &$val; return true; } /** * Returns only those array items, which matches a regular expression $pattern. * @param string[] $array * @return string[] */ public static function grep( array $array, #[Language('RegExp')] string $pattern, bool|int $invert = false, ): array { $flags = $invert ? PREG_GREP_INVERT : 0; return Strings::pcre('preg_grep', [$pattern, $array, $flags]); } /** * Transforms multidimensional array to flat array. */ public static function flatten(array $array, bool $preserveKeys = false): array { $res = []; $cb = $preserveKeys ? function ($v, $k) use (&$res): void { $res[$k] = $v; } : function ($v) use (&$res): void { $res[] = $v; }; array_walk_recursive($array, $cb); return $res; } /** * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list. */ public static function isList(mixed $value): bool { return is_array($value) && (PHP_VERSION_ID < 80100 ? !$value || array_keys($value) === range(0, count($value) - 1) : array_is_list($value) ); } /** * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. * @param string|string[] $path */ public static function associate(array $array, $path): array|\stdClass { $parts = is_array($path) ? $path : preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') { throw new Nette\InvalidArgumentException("Invalid path '$path'."); } $res = $parts[0] === '->' ? new \stdClass : []; foreach ($array as $rowOrig) { $row = (array) $rowOrig; $x = &$res; for ($i = 0; $i < count($parts); $i++) { $part = $parts[$i]; if ($part === '[]') { $x = &$x[]; } elseif ($part === '=') { if (isset($parts[++$i])) { $x = $row[$parts[$i]]; $row = null; } } elseif ($part === '->') { if (isset($parts[++$i])) { if ($x === null) { $x = new \stdClass; } $x = &$x->{$row[$parts[$i]]}; } else { $row = is_object($rowOrig) ? $rowOrig : (object) $row; } } elseif ($part !== '|') { $x = &$x[(string) $row[$part]]; } } if ($x === null) { $x = $row; } } return $res; } /** * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. */ public static function normalize(array $array, mixed $filling = null): array { $res = []; foreach ($array as $k => $v) { $res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v; } return $res; } /** * Returns and removes the value of an item from an array. If it does not exist, it throws an exception, * or returns $default, if provided. * @template T * @param array $array * @param ?T $default * @return ?T * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided */ public static function pick(array &$array, string|int $key, mixed $default = null): mixed { if (array_key_exists($key, $array)) { $value = $array[$key]; unset($array[$key]); return $value; } elseif (func_num_args() < 3) { throw new Nette\InvalidArgumentException("Missing item '$key'."); } else { return $default; } } /** * Tests whether at least one element in the array passes the test implemented by the * provided callback with signature `function ($value, $key, array $array): bool`. */ public static function some(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if ($callback($v, $k, $array)) { return true; } } return false; } /** * Tests whether all elements in the array pass the test implemented by the provided function, * which has the signature `function ($value, $key, array $array): bool`. */ public static function every(iterable $array, callable $callback): bool { foreach ($array as $k => $v) { if (!$callback($v, $k, $array)) { return false; } } return true; } /** * Calls $callback on all elements in the array and returns the array of return values. * The callback has the signature `function ($value, $key, array $array): bool`. */ public static function map(iterable $array, callable $callback): array { $res = []; foreach ($array as $k => $v) { $res[$k] = $callback($v, $k, $array); } return $res; } /** * Invokes all callbacks and returns array of results. * @param callable[] $callbacks */ public static function invoke(iterable $callbacks, ...$args): array { $res = []; foreach ($callbacks as $k => $cb) { $res[$k] = $cb(...$args); } return $res; } /** * Invokes method on every object in an array and returns array of results. * @param object[] $objects */ public static function invokeMethod(iterable $objects, string $method, ...$args): array { $res = []; foreach ($objects as $k => $obj) { $res[$k] = $obj->$method(...$args); } return $res; } /** * Copies the elements of the $array array to the $object object and then returns it. * @template T of object * @param T $object * @return T */ public static function toObject(iterable $array, object $object): object { foreach ($array as $k => $v) { $object->$k = $v; } return $object; } /** * Converts value to array key. */ public static function toKey(mixed $value): int|string { return key([$value => null]); } /** * Returns copy of the $array where every item is converted to string * and prefixed by $prefix and suffixed by $suffix. * @param string[] $array * @return string[] */ public static function wrap(array $array, string $prefix = '', string $suffix = ''): array { $res = []; foreach ($array as $k => $v) { $res[$k] = $prefix . $v . $suffix; } return $res; } } Utils/Type.php000064400000014316150250563100007300 0ustar00 */ private array $types; private bool $simple; private string $kind; // | & /** * Creates a Type object based on reflection. Resolves self, static and parent to the actual class name. * If the subject has no type, it returns null. */ public static function fromReflection( \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $reflection, ): ?self { $type = $reflection instanceof \ReflectionFunctionAbstract ? $reflection->getReturnType() ?? (PHP_VERSION_ID >= 80100 && $reflection instanceof \ReflectionMethod ? $reflection->getTentativeReturnType() : null) : $reflection->getType(); return $type ? self::fromReflectionType($type, $reflection, true) : null; } private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string { if ($type instanceof \ReflectionNamedType) { $name = self::resolve($type->getName(), $of); return $asObject ? new self($type->allowsNull() && $name !== 'mixed' ? [$name, 'null'] : [$name]) : $name; } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { return new self( array_map(fn($t) => self::fromReflectionType($t, $of, false), $type->getTypes()), $type instanceof \ReflectionUnionType ? '|' : '&', ); } else { throw new Nette\InvalidStateException('Unexpected type of ' . Reflection::toString($of)); } } /** * Creates the Type object according to the text notation. */ public static function fromString(string $type): self { if (!Validators::isTypeDeclaration($type)) { throw new Nette\InvalidArgumentException("Invalid type '$type'."); } if ($type[0] === '?') { return new self([substr($type, 1), 'null']); } $unions = []; foreach (explode('|', $type) as $part) { $part = explode('&', trim($part, '()')); $unions[] = count($part) === 1 ? $part[0] : new self($part, '&'); } return count($unions) === 1 && $unions[0] instanceof self ? $unions[0] : new self($unions); } /** * Resolves 'self', 'static' and 'parent' to the actual class name. */ public static function resolve( string $type, \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of, ): string { $lower = strtolower($type); if ($of instanceof \ReflectionFunction) { return $type; } elseif ($lower === 'self' || $lower === 'static') { return $of->getDeclaringClass()->name; } elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) { return $of->getDeclaringClass()->getParentClass()->name; } else { return $type; } } private function __construct(array $types, string $kind = '|') { $o = array_search('null', $types, true); if ($o !== false) { // null as last array_splice($types, $o, 1); $types[] = 'null'; } $this->types = $types; $this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null'; $this->kind = count($types) > 1 ? $kind : ''; } public function __toString(): string { $multi = count($this->types) > 1; if ($this->simple) { return ($multi ? '?' : '') . $this->types[0]; } $res = []; foreach ($this->types as $type) { $res[] = $type instanceof self && $multi ? "($type)" : $type; } return implode($this->kind, $res); } /** * Returns the array of subtypes that make up the compound type as strings. * @return array */ public function getNames(): array { return array_map(fn($t) => $t instanceof self ? $t->getNames() : $t, $this->types); } /** * Returns the array of subtypes that make up the compound type as Type objects: * @return self[] */ public function getTypes(): array { return array_map(fn($t) => $t instanceof self ? $t : new self([$t]), $this->types); } /** * Returns the type name for simple types, otherwise null. */ public function getSingleName(): ?string { return $this->simple ? $this->types[0] : null; } /** * Returns true whether it is a union type. */ public function isUnion(): bool { return $this->kind === '|'; } /** * Returns true whether it is an intersection type. */ public function isIntersection(): bool { return $this->kind === '&'; } /** * Returns true whether it is a simple type. Single nullable types are also considered to be simple types. */ public function isSimple(): bool { return $this->simple; } /** @deprecated use isSimple() */ public function isSingle(): bool { return $this->simple; } /** * Returns true whether the type is both a simple and a PHP built-in type. */ public function isBuiltin(): bool { return $this->simple && Validators::isBuiltinType($this->types[0]); } /** * Returns true whether the type is both a simple and a class name. */ public function isClass(): bool { return $this->simple && !Validators::isBuiltinType($this->types[0]); } /** * Determines if type is special class name self/parent/static. */ public function isClassKeyword(): bool { return $this->simple && Validators::isClassKeyword($this->types[0]); } /** * Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter. */ public function allows(string $subtype): bool { if ($this->types === ['mixed']) { return true; } $subtype = self::fromString($subtype); return $subtype->isUnion() ? Arrays::every($subtype->types, fn($t) => $this->allows2($t instanceof self ? $t->types : [$t])) : $this->allows2($subtype->types); } private function allows2(array $subtypes): bool { return $this->isUnion() ? Arrays::some($this->types, fn($t) => $this->allows3($t instanceof self ? $t->types : [$t], $subtypes)) : $this->allows3($this->types, $subtypes); } private function allows3(array $types, array $subtypes): bool { return Arrays::every( $types, fn($type) => Arrays::some( $subtypes, fn($subtype) => Validators::isBuiltinType($type) ? strcasecmp($type, $subtype) === 0 : is_a($subtype, $type, true) ) ); } } Utils/Validators.php000064400000024127150250563100010470 0ustar00 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, 'callable' => 1, 'iterable' => 1, 'void' => 1, 'null' => 1, 'mixed' => 1, 'false' => 1, 'never' => 1, 'true' => 1, ]; /** @var array */ protected static $validators = [ // PHP types 'array' => 'is_array', 'bool' => 'is_bool', 'boolean' => 'is_bool', 'float' => 'is_float', 'int' => 'is_int', 'integer' => 'is_int', 'null' => 'is_null', 'object' => 'is_object', 'resource' => 'is_resource', 'scalar' => 'is_scalar', 'string' => 'is_string', // pseudo-types 'callable' => [self::class, 'isCallable'], 'iterable' => 'is_iterable', 'list' => [Arrays::class, 'isList'], 'mixed' => [self::class, 'isMixed'], 'none' => [self::class, 'isNone'], 'number' => [self::class, 'isNumber'], 'numeric' => [self::class, 'isNumeric'], 'numericint' => [self::class, 'isNumericInt'], // string patterns 'alnum' => 'ctype_alnum', 'alpha' => 'ctype_alpha', 'digit' => 'ctype_digit', 'lower' => 'ctype_lower', 'pattern' => null, 'space' => 'ctype_space', 'unicode' => [self::class, 'isUnicode'], 'upper' => 'ctype_upper', 'xdigit' => 'ctype_xdigit', // syntax validation 'email' => [self::class, 'isEmail'], 'identifier' => [self::class, 'isPhpIdentifier'], 'uri' => [self::class, 'isUri'], 'url' => [self::class, 'isUrl'], // environment validation 'class' => 'class_exists', 'interface' => 'interface_exists', 'directory' => 'is_dir', 'file' => 'is_file', 'type' => [self::class, 'isType'], ]; /** @var array */ protected static $counters = [ 'string' => 'strlen', 'unicode' => [Strings::class, 'length'], 'array' => 'count', 'list' => 'count', 'alnum' => 'strlen', 'alpha' => 'strlen', 'digit' => 'strlen', 'lower' => 'strlen', 'space' => 'strlen', 'upper' => 'strlen', 'xdigit' => 'strlen', ]; /** * Verifies that the value is of expected types separated by pipe. * @throws AssertionException */ public static function assert(mixed $value, string $expected, string $label = 'variable'): void { if (!static::is($value, $expected)) { $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); $translate = ['boolean' => 'bool', 'integer' => 'int', 'double' => 'float', 'NULL' => 'null']; $type = $translate[gettype($value)] ?? gettype($value); if (is_int($value) || is_float($value) || (is_string($value) && strlen($value) < 40)) { $type .= ' ' . var_export($value, true); } elseif (is_object($value)) { $type .= ' ' . $value::class; } throw new AssertionException("The $label expects to be $expected, $type given."); } } /** * Verifies that element $key in array is of expected types separated by pipe. * @param mixed[] $array * @throws AssertionException */ public static function assertField( array $array, $key, ?string $expected = null, string $label = "item '%' in array", ): void { if (!array_key_exists($key, $array)) { throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.'); } elseif ($expected) { static::assert($array[$key], $expected, str_replace('%', $key, $label)); } } /** * Verifies that the value is of expected types separated by pipe. */ public static function is(mixed $value, string $expected): bool { foreach (explode('|', $expected) as $item) { if (str_ends_with($item, '[]')) { if (is_iterable($value) && self::everyIs($value, substr($item, 0, -2))) { return true; } continue; } elseif (str_starts_with($item, '?')) { $item = substr($item, 1); if ($value === null) { return true; } } [$type] = $item = explode(':', $item, 2); if (isset(static::$validators[$type])) { try { if (!static::$validators[$type]($value)) { continue; } } catch (\TypeError $e) { continue; } } elseif ($type === 'pattern') { if (Strings::match($value, '|^' . ($item[1] ?? '') . '$|D')) { return true; } continue; } elseif (!$value instanceof $type) { continue; } if (isset($item[1])) { $length = $value; if (isset(static::$counters[$type])) { $length = static::$counters[$type]($value); } $range = explode('..', $item[1]); if (!isset($range[1])) { $range[1] = $range[0]; } if (($range[0] !== '' && $length < $range[0]) || ($range[1] !== '' && $length > $range[1])) { continue; } } return true; } return false; } /** * Finds whether all values are of expected types separated by pipe. * @param mixed[] $values */ public static function everyIs(iterable $values, string $expected): bool { foreach ($values as $value) { if (!static::is($value, $expected)) { return false; } } return true; } /** * Checks if the value is an integer or a float. */ public static function isNumber(mixed $value): bool { return is_int($value) || is_float($value); } /** * Checks if the value is an integer or a integer written in a string. */ public static function isNumericInt(mixed $value): bool { return is_int($value) || (is_string($value) && preg_match('#^[+-]?[0-9]+$#D', $value)); } /** * Checks if the value is a number or a number written in a string. */ public static function isNumeric(mixed $value): bool { return is_float($value) || is_int($value) || (is_string($value) && preg_match('#^[+-]?([0-9]++\.?[0-9]*|\.[0-9]+)$#D', $value)); } /** * Checks if the value is a syntactically correct callback. */ public static function isCallable(mixed $value): bool { return $value && is_callable($value, true); } /** * Checks if the value is a valid UTF-8 string. */ public static function isUnicode(mixed $value): bool { return is_string($value) && preg_match('##u', $value); } /** * Checks if the value is 0, '', false or null. */ public static function isNone(mixed $value): bool { return $value == null; // intentionally == } /** @internal */ public static function isMixed(): bool { return true; } /** * Checks if a variable is a zero-based integer indexed array. * @deprecated use Nette\Utils\Arrays::isList */ public static function isList(mixed $value): bool { return Arrays::isList($value); } /** * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). * Numbers, strings and DateTime objects can be compared. */ public static function isInRange(mixed $value, array $range): bool { if ($value === null || !(isset($range[0]) || isset($range[1]))) { return false; } $limit = $range[0] ?? $range[1]; if (is_string($limit)) { $value = (string) $value; } elseif ($limit instanceof \DateTimeInterface) { if (!$value instanceof \DateTimeInterface) { return false; } } elseif (is_numeric($value)) { $value *= 1; } else { return false; } return (!isset($range[0]) || ($value >= $range[0])) && (!isset($range[1]) || ($value <= $range[1])); } /** * Checks if the value is a valid email address. It does not verify that the domain actually exists, only the syntax is verified. */ public static function isEmail(string $value): bool { $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part $alpha = "a-z\x80-\xFF"; // superset of IDN return (bool) preg_match(<< \\? (? [a-zA-Z_\x7f-\xff][\w\x7f-\xff]*) (\\ (?&name))* ) | (? (?&type) (& (?&type))+ ) | (? (?&type) | \( (?&intersection) \) ) (\| (?&upart))+ )$~xAD XX, $type); } } Utils/exceptions.php000064400000001407150250563100010535 0ustar00 $array */ public static function from(array $array, bool $recursive = true): static { $obj = new static; foreach ($array as $key => $value) { $obj->$key = $recursive && is_array($value) ? static::from($value, true) : $value; } return $obj; } /** * Returns an iterator over all items. * @return \Iterator */ public function &getIterator(): \Iterator { foreach ((array) $this as $key => $foo) { yield $key => $this->$key; } } /** * Returns items count. */ public function count(): int { return count((array) $this); } /** * Replaces or appends a item. * @param string|int $key * @param T $value */ public function offsetSet($key, $value): void { if (!is_scalar($key)) { // prevents null throw new Nette\InvalidArgumentException(sprintf('Key must be either a string or an integer, %s given.', gettype($key))); } $this->$key = $value; } /** * Returns a item. * @param string|int $key * @return T */ #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->$key; } /** * Determines whether a item exists. * @param string|int $key */ public function offsetExists($key): bool { return isset($this->$key); } /** * Removes the element from this list. * @param string|int $key */ public function offsetUnset($key): void { unset($this->$key); } } Utils/DateTime.php000064400000005624150250563100010055 0ustar00format('Y-m-d H:i:s.u'), $time->getTimezone()); } elseif (is_numeric($time)) { if ($time <= self::YEAR) { $time += time(); } return (new static('@' . $time))->setTimezone(new \DateTimeZone(date_default_timezone_get())); } else { // textual or null return new static((string) $time); } } /** * Creates DateTime object. * @throws Nette\InvalidArgumentException if the date and time are not valid. */ public static function fromParts( int $year, int $month, int $day, int $hour = 0, int $minute = 0, float $second = 0.0, ): static { $s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second); if ( !checkdate($month, $day, $year) || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59 || $second < 0 || $second >= 60 ) { throw new Nette\InvalidArgumentException("Invalid date '$s'"); } return new static($s); } /** * Returns new DateTime object formatted according to the specified format. */ public static function createFromFormat( string $format, string $time, string|\DateTimeZone|null $timezone = null, ): static|false { if ($timezone === null) { $timezone = new \DateTimeZone(date_default_timezone_get()); } elseif (is_string($timezone)) { $timezone = new \DateTimeZone($timezone); } $date = parent::createFromFormat($format, $time, $timezone); return $date ? static::from($date) : false; } /** * Returns JSON representation in ISO 8601 (used by JavaScript). */ public function jsonSerialize(): string { return $this->format('c'); } /** * Returns the date and time in the format 'Y-m-d H:i:s'. */ public function __toString(): string { return $this->format('Y-m-d H:i:s'); } /** * Creates a copy with a modified time. */ public function modifyClone(string $modify = ''): static { $dolly = clone $this; return $modify ? $dolly->modify($modify) : $dolly; } } Utils/Helpers.php000064400000005000150250563100007747 0ustar00 $max) { throw new Nette\InvalidArgumentException("Minimum ($min) is not less than maximum ($max)."); } return min(max($value, $min), $max); } /** * Looks for a string from possibilities that is most similar to value, but not the same (for 8-bit encoding). * @param string[] $possibilities */ public static function getSuggestion(array $possibilities, string $value): ?string { $best = null; $min = (strlen($value) / 4 + 1) * 10 + .1; foreach (array_unique($possibilities) as $item) { if ($item !== $value && ($len = levenshtein($item, $value, 10, 11, 10)) < $min) { $min = $len; $best = $item; } } return $best; } /** * Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <> */ public static function compare(mixed $left, string $operator, mixed $right): bool { return match ($operator) { '>' => $left > $right, '>=' => $left >= $right, '<' => $left < $right, '<=' => $left <= $right, '=', '==' => $left == $right, '===' => $left === $right, '!=', '<>' => $left != $right, '!==' => $left !== $right, default => throw new Nette\InvalidArgumentException("Unknown operator '$operator'"), }; } } Utils/Html.php000064400000046030150250563100007261 0ustar00 element's attributes */ public $attrs = []; /** void elements */ public static $emptyElements = [ 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1, ]; /** @var array nodes */ protected $children = []; /** element's name */ private string $name = ''; private bool $isEmpty = false; /** * Constructs new HTML element. * @param array|string $attrs element's attributes or plain text content */ public static function el(?string $name = null, array|string|null $attrs = null): static { $el = new static; $parts = explode(' ', (string) $name, 2); $el->setName($parts[0]); if (is_array($attrs)) { $el->attrs = $attrs; } elseif ($attrs !== null) { $el->setText($attrs); } if (isset($parts[1])) { foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\2|\s))?#i') as $m) { $el->attrs[$m[1]] = $m[3] ?? true; } } return $el; } /** * Returns an object representing HTML text. */ public static function fromHtml(string $html): static { return (new static)->setHtml($html); } /** * Returns an object representing plain text. */ public static function fromText(string $text): static { return (new static)->setText($text); } /** * Converts to HTML. */ final public function toHtml(): string { return $this->render(); } /** * Converts to plain text. */ final public function toText(): string { return $this->getText(); } /** * Converts given HTML code to plain text. */ public static function htmlToText(string $html): string { return html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * Changes element's name. */ final public function setName(string $name, ?bool $isEmpty = null): static { $this->name = $name; $this->isEmpty = $isEmpty ?? isset(static::$emptyElements[$name]); return $this; } /** * Returns element's name. */ final public function getName(): string { return $this->name; } /** * Is element empty? */ final public function isEmpty(): bool { return $this->isEmpty; } /** * Sets multiple attributes. */ public function addAttributes(array $attrs): static { $this->attrs = array_merge($this->attrs, $attrs); return $this; } /** * Appends value to element's attribute. */ public function appendAttribute(string $name, mixed $value, mixed $option = true): static { if (is_array($value)) { $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : []; $this->attrs[$name] = $value + $prev; } elseif ((string) $value === '') { $tmp = &$this->attrs[$name]; // appending empty value? -> ignore, but ensure it exists } elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) { // needs array $this->attrs[$name][$value] = $option; } else { $this->attrs[$name] = [$this->attrs[$name] => true, $value => $option]; } return $this; } /** * Sets element's attribute. */ public function setAttribute(string $name, mixed $value): static { $this->attrs[$name] = $value; return $this; } /** * Returns element's attribute. */ public function getAttribute(string $name): mixed { return $this->attrs[$name] ?? null; } /** * Unsets element's attribute. */ public function removeAttribute(string $name): static { unset($this->attrs[$name]); return $this; } /** * Unsets element's attributes. */ public function removeAttributes(array $attributes): static { foreach ($attributes as $name) { unset($this->attrs[$name]); } return $this; } /** * Overloaded setter for element's attribute. */ final public function __set(string $name, mixed $value): void { $this->attrs[$name] = $value; } /** * Overloaded getter for element's attribute. */ final public function &__get(string $name): mixed { return $this->attrs[$name]; } /** * Overloaded tester for element's attribute. */ final public function __isset(string $name): bool { return isset($this->attrs[$name]); } /** * Overloaded unsetter for element's attribute. */ final public function __unset(string $name): void { unset($this->attrs[$name]); } /** * Overloaded setter for element's attribute. */ final public function __call(string $m, array $args): mixed { $p = substr($m, 0, 3); if ($p === 'get' || $p === 'set' || $p === 'add') { $m = substr($m, 3); $m[0] = $m[0] | "\x20"; if ($p === 'get') { return $this->attrs[$m] ?? null; } elseif ($p === 'add') { $args[] = true; } } if (count($args) === 0) { // invalid } elseif (count($args) === 1) { // set $this->attrs[$m] = $args[0]; } else { // add $this->appendAttribute($m, $args[0], $args[1]); } return $this; } /** * Special setter for element's attribute. */ final public function href(string $path, array $query = []): static { if ($query) { $query = http_build_query($query, '', '&'); if ($query !== '') { $path .= '?' . $query; } } $this->attrs['href'] = $path; return $this; } /** * Setter for data-* attributes. Booleans are converted to 'true' resp. 'false'. */ public function data(string $name, mixed $value = null): static { if (func_num_args() === 1) { $this->attrs['data'] = $name; } else { $this->attrs["data-$name"] = is_bool($value) ? json_encode($value) : $value; } return $this; } /** * Sets element's HTML content. */ final public function setHtml(mixed $html): static { $this->children = [(string) $html]; return $this; } /** * Returns element's HTML content. */ final public function getHtml(): string { return implode('', $this->children); } /** * Sets element's textual content. */ final public function setText(mixed $text): static { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); } $this->children = [(string) $text]; return $this; } /** * Returns element's textual content. */ final public function getText(): string { return self::htmlToText($this->getHtml()); } /** * Adds new element's child. */ final public function addHtml(mixed $child): static { return $this->insert(null, $child); } /** * Appends plain-text string to element content. */ public function addText(mixed $text): static { if (!$text instanceof HtmlStringable) { $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8'); } return $this->insert(null, $text); } /** * Creates and adds a new Html child. */ final public function create(string $name, array|string|null $attrs = null): static { $this->insert(null, $child = static::el($name, $attrs)); return $child; } /** * Inserts child node. */ public function insert(?int $index, HtmlStringable|string $child, bool $replace = false): static { $child = $child instanceof self ? $child : (string) $child; if ($index === null) { // append $this->children[] = $child; } else { // insert or replace array_splice($this->children, $index, $replace ? 1 : 0, [$child]); } return $this; } /** * Inserts (replaces) child node (\ArrayAccess implementation). * @param int|null $index position or null for appending * @param Html|string $child Html node or raw HTML string */ final public function offsetSet($index, $child): void { $this->insert($index, $child, true); } /** * Returns child node (\ArrayAccess implementation). * @param int $index */ final public function offsetGet($index): HtmlStringable|string { return $this->children[$index]; } /** * Exists child node? (\ArrayAccess implementation). * @param int $index */ final public function offsetExists($index): bool { return isset($this->children[$index]); } /** * Removes child node (\ArrayAccess implementation). * @param int $index */ public function offsetUnset($index): void { if (isset($this->children[$index])) { array_splice($this->children, $index, 1); } } /** * Returns children count. */ final public function count(): int { return count($this->children); } /** * Removes all children. */ public function removeChildren(): void { $this->children = []; } /** * Iterates over elements. * @return \ArrayIterator */ final public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->children); } /** * Returns all children. */ final public function getChildren(): array { return $this->children; } /** * Renders element's start tag, content and end tag. */ final public function render(?int $indent = null): string { $s = $this->startTag(); if (!$this->isEmpty) { // add content if ($indent !== null) { $indent++; } foreach ($this->children as $child) { if ($child instanceof self) { $s .= $child->render($indent); } else { $s .= $child; } } // add end tag $s .= $this->endTag(); } if ($indent !== null) { return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2)); } return $s; } final public function __toString(): string { return $this->render(); } /** * Returns element's start tag. */ final public function startTag(): string { return $this->name ? '<' . $this->name . $this->attributes() . '>' : ''; } /** * Returns element's end tag. */ final public function endTag(): string { return $this->name && !$this->isEmpty ? 'name . '>' : ''; } /** * Returns element's attributes. * @internal */ final public function attributes(): string { if (!is_array($this->attrs)) { return ''; } $s = ''; $attrs = $this->attrs; foreach ($attrs as $key => $value) { if ($value === null || $value === false) { continue; } elseif ($value === true) { $s .= ' ' . $key; continue; } elseif (is_array($value)) { if (strncmp($key, 'data-', 5) === 0) { $value = Json::encode($value); } else { $tmp = null; foreach ($value as $k => $v) { if ($v != null) { // intentionally ==, skip nulls & empty string // composite 'style' vs. 'others' $tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v); } } if ($tmp === null) { continue; } $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp); } } elseif (is_float($value)) { $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.'); } else { $value = (string) $value; } $q = str_contains($value, '"') ? "'" : '"'; $s .= ' ' . $key . '=' . $q . str_replace( ['&', $q, '<'], ['&', $q === '"' ? '"' : ''', '<'], $value, ) . (str_contains($value, '`') && strpbrk($value, ' <>"\'') === false ? ' ' : '') . $q; } $s = str_replace('@', '@', $s); return $s; } /** * Clones all children too. */ public function __clone() { foreach ($this->children as $key => $value) { if (is_object($value)) { $this->children[$key] = clone $value; } } } } Utils/Reflection.php000064400000020544150250563100010451 0ustar00isDefaultValueConstant()) { $const = $orig = $param->getDefaultValueConstantName(); $pair = explode('::', $const); if (isset($pair[1])) { $pair[0] = Type::resolve($pair[0], $param); try { $rcc = new \ReflectionClassConstant($pair[0], $pair[1]); } catch (\ReflectionException $e) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name.", 0, $e); } return $rcc->getValue(); } elseif (!defined($const)) { $const = substr((string) strrchr($const, '\\'), 1); if (!defined($const)) { $name = self::toString($param); throw new \ReflectionException("Unable to resolve constant $orig used as default value of $name."); } } return constant($const); } return $param->getDefaultValue(); } /** * Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait. */ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass { foreach ($prop->getDeclaringClass()->getTraits() as $trait) { if ($trait->hasProperty($prop->name) // doc-comment guessing as workaround for insufficient PHP reflection && $trait->getProperty($prop->name)->getDocComment() === $prop->getDocComment() ) { return self::getPropertyDeclaringClass($trait->getProperty($prop->name)); } } return $prop->getDeclaringClass(); } /** * Returns a reflection of a method that contains a declaration of $method. * Usually, each method is its own declaration, but the body of the method can also be in the trait and under a different name. */ public static function getMethodDeclaringMethod(\ReflectionMethod $method): \ReflectionMethod { // file & line guessing as workaround for insufficient PHP reflection $decl = $method->getDeclaringClass(); if ($decl->getFileName() === $method->getFileName() && $decl->getStartLine() <= $method->getStartLine() && $decl->getEndLine() >= $method->getEndLine() ) { return $method; } $hash = [$method->getFileName(), $method->getStartLine(), $method->getEndLine()]; if (($alias = $decl->getTraitAliases()[$method->name] ?? null) && ($m = new \ReflectionMethod($alias)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } foreach ($decl->getTraits() as $trait) { if ($trait->hasMethod($method->name) && ($m = $trait->getMethod($method->name)) && $hash === [$m->getFileName(), $m->getStartLine(), $m->getEndLine()] ) { return self::getMethodDeclaringMethod($m); } } return $method; } /** * Finds out if reflection has access to PHPdoc comments. Comments may not be available due to the opcode cache. */ public static function areCommentsAvailable(): bool { static $res; return $res ?? $res = (bool) (new \ReflectionMethod(__METHOD__))->getDocComment(); } public static function toString(\Reflector $ref): string { if ($ref instanceof \ReflectionClass) { return $ref->name; } elseif ($ref instanceof \ReflectionMethod) { return $ref->getDeclaringClass()->name . '::' . $ref->name . '()'; } elseif ($ref instanceof \ReflectionFunction) { return $ref->name . '()'; } elseif ($ref instanceof \ReflectionProperty) { return self::getPropertyDeclaringClass($ref)->name . '::$' . $ref->name; } elseif ($ref instanceof \ReflectionParameter) { return '$' . $ref->name . ' in ' . self::toString($ref->getDeclaringFunction()); } else { throw new Nette\InvalidArgumentException; } } /** * Expands the name of the class to full name in the given context of given class. * Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context. * @throws Nette\InvalidArgumentException */ public static function expandClassName(string $name, \ReflectionClass $context): string { $lower = strtolower($name); if (empty($name)) { throw new Nette\InvalidArgumentException('Class name must not be empty.'); } elseif (Validators::isBuiltinType($lower)) { return $lower; } elseif ($lower === 'self' || $lower === 'static') { return $context->name; } elseif ($lower === 'parent') { return $context->getParentClass() ? $context->getParentClass()->name : 'parent'; } elseif ($name[0] === '\\') { // fully qualified name return ltrim($name, '\\'); } $uses = self::getUseStatements($context); $parts = explode('\\', $name, 2); if (isset($uses[$parts[0]])) { $parts[0] = $uses[$parts[0]]; return implode('\\', $parts); } elseif ($context->inNamespace()) { return $context->getNamespaceName() . '\\' . $name; } else { return $name; } } /** @return array of [alias => class] */ public static function getUseStatements(\ReflectionClass $class): array { if ($class->isAnonymous()) { throw new Nette\NotImplementedException('Anonymous classes are not supported.'); } static $cache = []; if (!isset($cache[$name = $class->name])) { if ($class->isInternal()) { $cache[$name] = []; } else { $code = file_get_contents($class->getFileName()); $cache = self::parseUseStatements($code, $name) + $cache; } } return $cache[$name]; } /** * Parses PHP code to [class => [alias => class, ...]] */ private static function parseUseStatements(string $code, ?string $forClass = null): array { try { $tokens = \PhpToken::tokenize($code, TOKEN_PARSE); } catch (\ParseError $e) { trigger_error($e->getMessage(), E_USER_NOTICE); $tokens = []; } $namespace = $class = $classLevel = $level = null; $res = $uses = []; $nameTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED]; while ($token = current($tokens)) { next($tokens); switch ($token->id) { case T_NAMESPACE: $namespace = ltrim(self::fetch($tokens, $nameTokens) . '\\', '\\'); $uses = []; break; case T_CLASS: case T_INTERFACE: case T_TRAIT: case PHP_VERSION_ID < 80100 ? T_CLASS : T_ENUM: if ($name = self::fetch($tokens, T_STRING)) { $class = $namespace . $name; $classLevel = $level + 1; $res[$class] = $uses; if ($class === $forClass) { return $res; } } break; case T_USE: while (!$class && ($name = self::fetch($tokens, $nameTokens))) { $name = ltrim($name, '\\'); if (self::fetch($tokens, '{')) { while ($suffix = self::fetch($tokens, $nameTokens)) { if (self::fetch($tokens, T_AS)) { $uses[self::fetch($tokens, T_STRING)] = $name . $suffix; } else { $tmp = explode('\\', $suffix); $uses[end($tmp)] = $name . $suffix; } if (!self::fetch($tokens, ',')) { break; } } } elseif (self::fetch($tokens, T_AS)) { $uses[self::fetch($tokens, T_STRING)] = $name; } else { $tmp = explode('\\', $name); $uses[end($tmp)] = $name; } if (!self::fetch($tokens, ',')) { break; } } break; case T_CURLY_OPEN: case T_DOLLAR_OPEN_CURLY_BRACES: case ord('{'): $level++; break; case ord('}'): if ($level === $classLevel) { $class = $classLevel = null; } $level--; } } return $res; } private static function fetch(array &$tokens, string|int|array $take): ?string { $res = null; while ($token = current($tokens)) { if ($token->is($take)) { $res .= $token->text; } elseif (!$token->is([T_DOC_COMMENT, T_WHITESPACE, T_COMMENT])) { break; } next($tokens); } return $res; } } Utils/Json.php000064400000004342150250563100007266 0ustar00page = $page; return $this; } /** * Returns current page number. */ public function getPage(): int { return $this->base + $this->getPageIndex(); } /** * Returns first page number. */ public function getFirstPage(): int { return $this->base; } /** * Returns last page number. */ public function getLastPage(): ?int { return $this->itemCount === null ? null : $this->base + max(0, $this->getPageCount() - 1); } /** * Returns the sequence number of the first element on the page */ public function getFirstItemOnPage(): int { return $this->itemCount !== 0 ? $this->offset + 1 : 0; } /** * Returns the sequence number of the last element on the page */ public function getLastItemOnPage(): int { return $this->offset + $this->length; } /** * Sets first page (base) number. */ public function setBase(int $base): static { $this->base = $base; return $this; } /** * Returns first page (base) number. */ public function getBase(): int { return $this->base; } /** * Returns zero-based page number. */ protected function getPageIndex(): int { $index = max(0, $this->page - $this->base); return $this->itemCount === null ? $index : min($index, max(0, $this->getPageCount() - 1)); } /** * Is the current page the first one? */ public function isFirst(): bool { return $this->getPageIndex() === 0; } /** * Is the current page the last one? */ public function isLast(): bool { return $this->itemCount === null ? false : $this->getPageIndex() >= $this->getPageCount() - 1; } /** * Returns the total number of pages. */ public function getPageCount(): ?int { return $this->itemCount === null ? null : (int) ceil($this->itemCount / $this->itemsPerPage); } /** * Sets the number of items to display on a single page. */ public function setItemsPerPage(int $itemsPerPage): static { $this->itemsPerPage = max(1, $itemsPerPage); return $this; } /** * Returns the number of items to display on a single page. */ public function getItemsPerPage(): int { return $this->itemsPerPage; } /** * Sets the total number of items. */ public function setItemCount(?int $itemCount = null): static { $this->itemCount = $itemCount === null ? null : max(0, $itemCount); return $this; } /** * Returns the total number of items. */ public function getItemCount(): ?int { return $this->itemCount; } /** * Returns the absolute index of the first item on current page. */ public function getOffset(): int { return $this->getPageIndex() * $this->itemsPerPage; } /** * Returns the absolute index of the first item on current page in countdown paging. */ public function getCountdownOffset(): ?int { return $this->itemCount === null ? null : max(0, $this->itemCount - ($this->getPageIndex() + 1) * $this->itemsPerPage); } /** * Returns the number of items on current page. */ public function getLength(): int { return $this->itemCount === null ? $this->itemsPerPage : min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage); } } Utils/Finder.php000064400000031501150250563100007561 0ustar00size('> 10kB') * ->from('.') * ->exclude('temp'); * * @implements \IteratorAggregate */ class Finder implements \IteratorAggregate { use Nette\SmartObject; /** @var array */ private array $find = []; /** @var string[] */ private array $in = []; /** @var \Closure[] */ private array $filters = []; /** @var \Closure[] */ private array $descentFilters = []; /** @var array */ private array $appends = []; private bool $childFirst = false; /** @var ?callable */ private $sort; private int $maxDepth = -1; private bool $ignoreUnreadableDirs = true; /** * Begins search for files and directories matching mask. */ public static function find(string|array $masks): static { $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic return (new static)->addMask($masks, 'dir')->addMask($masks, 'file'); } /** * Begins search for files matching mask. */ public static function findFiles(string|array $masks): static { $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic return (new static)->addMask($masks, 'file'); } /** * Begins search for directories matching mask. */ public static function findDirectories(string|array $masks): static { $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic return (new static)->addMask($masks, 'dir'); } /** * Finds files matching the specified masks. */ public function files(string|array $masks): static { return $this->addMask((array) $masks, 'file'); } /** * Finds directories matching the specified masks. */ public function directories(string|array $masks): static { return $this->addMask((array) $masks, 'dir'); } private function addMask(array $masks, string $mode): static { foreach ($masks as $mask) { $mask = FileSystem::unixSlashes($mask); if ($mode === 'dir') { $mask = rtrim($mask, '/'); } if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) { throw new Nette\InvalidArgumentException("Invalid mask '$mask'"); } if (str_starts_with($mask, '**/')) { $mask = substr($mask, 3); } $this->find[] = [$mask, $mode]; } return $this; } /** * Searches in the given directories. Wildcards are allowed. */ public function in(string|array $paths): static { $paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic $this->addLocation($paths, ''); return $this; } /** * Searches recursively from the given directories. Wildcards are allowed. */ public function from(string|array $paths): static { $paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic $this->addLocation($paths, '/**'); return $this; } private function addLocation(array $paths, string $ext): void { foreach ($paths as $path) { if ($path === '') { throw new Nette\InvalidArgumentException("Invalid directory '$path'"); } $path = rtrim(FileSystem::unixSlashes($path), '/'); $this->in[] = $path . $ext; } } /** * Lists directory's contents before the directory itself. By default, this is disabled. */ public function childFirst(bool $state = true): static { $this->childFirst = $state; return $this; } /** * Ignores unreadable directories. By default, this is enabled. */ public function ignoreUnreadableDirs(bool $state = true): static { $this->ignoreUnreadableDirs = $state; return $this; } /** * Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory. * @param callable(FileInfo, FileInfo): int $callback */ public function sortBy(callable $callback): static { $this->sort = $callback; return $this; } /** * Sorts files in each directory naturally by name. */ public function sortByName(): static { $this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename()); return $this; } /** * Adds the specified paths or appends a new finder that returns. */ public function append(string|array|null $paths = null): static { if ($paths === null) { return $this->appends[] = new static; } $this->appends = array_merge($this->appends, (array) $paths); return $this; } /********************* filtering ****************d*g**/ /** * Skips entries that matches the given masks relative to the ones defined with the in() or from() methods. */ public function exclude(string|array $masks): static { $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic foreach ($masks as $mask) { $mask = FileSystem::unixSlashes($mask); if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) { throw new Nette\InvalidArgumentException("Invalid mask '$mask'"); } $end = $m[3]; $re = $this->buildPattern($m[2]); $filter = fn(FileInfo $file): bool => ($end && !$file->isDir()) || !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname())); $this->descentFilter($filter); if ($end !== '/*') { $this->filter($filter); } } return $this; } /** * Yields only entries which satisfy the given filter. * @param callable(FileInfo): bool $callback */ public function filter(callable $callback): static { $this->filters[] = \Closure::fromCallable($callback); return $this; } /** * It descends only to directories that match the specified filter. * @param callable(FileInfo): bool $callback */ public function descentFilter(callable $callback): static { $this->descentFilters[] = \Closure::fromCallable($callback); return $this; } /** * Sets the maximum depth of entries. */ public function limitDepth(?int $depth): static { $this->maxDepth = $depth ?? -1; return $this; } /** * Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB */ public function size(string $operator, ?int $size = null): static { if (func_num_args() === 1) { // in $operator is predicate if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) { throw new Nette\InvalidArgumentException('Invalid size predicate format.'); } [, $operator, $size, $unit] = $matches; $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9]; $size *= $units[strtolower($unit)]; $operator = $operator ?: '='; } return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size)); } /** * Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23 */ public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static { if (func_num_args() === 1) { // in $operator is predicate if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) { throw new Nette\InvalidArgumentException('Invalid date predicate format.'); } [, $operator, $date] = $matches; $operator = $operator ?: '='; } $date = DateTime::from($date)->format('U'); return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date)); } /********************* iterator generator ****************d*g**/ /** * Returns an array with all found files and directories. */ public function collect(): array { return iterator_to_array($this->getIterator()); } /** @return \Generator */ public function getIterator(): \Generator { $plan = $this->buildPlan(); foreach ($plan as $dir => $searches) { yield from $this->traverseDir($dir, $searches); } foreach ($this->appends as $item) { if ($item instanceof self) { yield from $item->getIterator(); } else { $item = FileSystem::platformSlashes($item); yield $item => new FileInfo($item); } } } /** * @param array<\stdClass{pattern: string, mode: string, recursive: bool}> $searches * @param string[] $subdirs * @return \Generator */ private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator { if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) { return; } elseif (!is_dir($dir)) { throw new Nette\InvalidStateException("Directory '$dir' not found."); } try { $pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS); } catch (\UnexpectedValueException $e) { if ($this->ignoreUnreadableDirs) { return; } else { throw new Nette\InvalidStateException($e->getMessage()); } } $files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir)); if ($this->sort) { $files = iterator_to_array($files); usort($files, $this->sort); } foreach ($files as $file) { $pathName = $file->getPathname(); $cache = $subSearch = []; if ($file->isDir()) { foreach ($searches as $search) { if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) { $subSearch[] = $search; } } } if ($this->childFirst && $subSearch) { yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()])); } $relativePathname = FileSystem::unixSlashes($file->getRelativePathname()); foreach ($searches as $search) { if ( $file->getType() === $search->mode && preg_match($search->pattern, $relativePathname) && $this->proveFilters($this->filters, $file, $cache) ) { yield $pathName => $file; break; } } if (!$this->childFirst && $subSearch) { yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()])); } } } private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator { foreach ($pathNames as $pathName) { if (!$absolute) { $pathName = preg_replace('~\.?/~A', '', $pathName); } $pathName = FileSystem::platformSlashes($pathName); yield new FileInfo($pathName, $relativePath); } } private function proveFilters(array $filters, FileInfo $file, array &$cache): bool { foreach ($filters as $filter) { $res = &$cache[spl_object_id($filter)]; $res ??= $filter($file); if (!$res) { return false; } } return true; } /** @return array> */ private function buildPlan(): array { $plan = $dirCache = []; foreach ($this->find as [$mask, $mode]) { $splits = []; if (FileSystem::isAbsolute($mask)) { if ($this->in) { throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'."); } $splits[] = self::splitRecursivePart($mask); } else { foreach ($this->in ?: ['.'] as $in) { $in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob() $splits[] = self::splitRecursivePart($in . '/' . $mask); } } foreach ($splits as [$base, $rest, $recursive]) { $base = $base === '' ? '.' : $base; $dirs = $dirCache[$base] ??= strpbrk($base, '*?[') ? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE) : [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ] $search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive]; foreach ($dirs as $dir) { $plan[$dir][] = $search; } } } return $plan; } /** * Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal. */ private static function splitRecursivePart(string $path): array { $a = strrpos($path, '/'); $parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2); return isset($parts[1]) ? [$parts[0], $parts[1] . substr($path, $a + 1), true] : [$parts[0], substr($path, $a + 1), false]; } /** * Converts wildcards to regular expression. */ private function buildPattern(string $mask): string { if ($mask === '*') { return '##'; } elseif (str_starts_with($mask, './')) { $anchor = '^'; $mask = substr($mask, 2); } else { $anchor = '(?:^|/)'; } $pattern = strtr( preg_quote($mask, '#'), [ '\*\*/' => '(.+/)?', '\*' => '[^/]*', '\?' => '[^/]', '\[\!' => '[^', '\[' => '[', '\]' => ']', '\-' => '-', ], ); return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : ''); } } Utils/ArrayList.php000064400000005045150250563100010270 0ustar00 $array */ public static function from(array $array): static { if (!Arrays::isList($array)) { throw new Nette\InvalidArgumentException('Array is not valid list.'); } $obj = new static; $obj->list = $array; return $obj; } /** * Returns an iterator over all items. * @return \Iterator */ public function &getIterator(): \Iterator { foreach ($this->list as &$item) { yield $item; } } /** * Returns items count. */ public function count(): int { return count($this->list); } /** * Replaces or appends a item. * @param int|null $index * @param T $value * @throws Nette\OutOfRangeException */ public function offsetSet($index, $value): void { if ($index === null) { $this->list[] = $value; } elseif (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } else { $this->list[$index] = $value; } } /** * Returns a item. * @param int $index * @return T * @throws Nette\OutOfRangeException */ public function offsetGet($index): mixed { if (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } return $this->list[$index]; } /** * Determines whether a item exists. * @param int $index */ public function offsetExists($index): bool { return is_int($index) && $index >= 0 && $index < count($this->list); } /** * Removes the element at the specified position in this list. * @param int $index * @throws Nette\OutOfRangeException */ public function offsetUnset($index): void { if (!is_int($index) || $index < 0 || $index >= count($this->list)) { throw new Nette\OutOfRangeException('Offset invalid or out of range'); } array_splice($this->list, $index, 1); } /** * Prepends a item. * @param T $value */ public function prepend(mixed $value): void { $first = array_slice($this->list, 0, 1); $this->offsetSet(0, $value); array_splice($this->list, 1, 0, $first); } } Utils/ObjectHelpers.php000064400000015544150250563100011114 0ustar00getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), self::parseFullDoc($rc, '~^[ \t*]*@property(?:-read)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), ), $name); throw new MemberAccessException("Cannot read an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } /** * @return never * @throws MemberAccessException */ public static function strictSet(string $class, string $name): void { $rc = new \ReflectionClass($class); $hint = self::getSuggestion(array_merge( array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic()), self::parseFullDoc($rc, '~^[ \t*]*@property(?:-write)?[ \t]+(?:\S+[ \t]+)??\$(\w+)~m'), ), $name); throw new MemberAccessException("Cannot write to an undeclared property $class::\$$name" . ($hint ? ", did you mean \$$hint?" : '.')); } /** * @return never * @throws MemberAccessException */ public static function strictCall(string $class, string $method, array $additionalMethods = []): void { $trace = debug_backtrace(0, 3); // suppose this method is called from __call() $context = ($trace[1]['function'] ?? null) === '__call' ? ($trace[2]['class'] ?? null) : null; if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() $class = get_parent_class($context); } if (method_exists($class, $method)) { // insufficient visibility $rm = new \ReflectionMethod($class, $method); $visibility = $rm->isPrivate() ? 'private ' : ($rm->isProtected() ? 'protected ' : ''); throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); } else { $hint = self::getSuggestion(array_merge( get_class_methods($class), self::parseFullDoc(new \ReflectionClass($class), '~^[ \t*]*@method[ \t]+(?:static[ \t]+)?(?:\S+[ \t]+)??(\w+)\(~m'), $additionalMethods, ), $method); throw new MemberAccessException("Call to undefined method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } } /** * @return never * @throws MemberAccessException */ public static function strictStaticCall(string $class, string $method): void { $trace = debug_backtrace(0, 3); // suppose this method is called from __callStatic() $context = ($trace[1]['function'] ?? null) === '__callStatic' ? ($trace[2]['class'] ?? null) : null; if ($context && is_a($class, $context, true) && method_exists($context, $method)) { // called parent::$method() $class = get_parent_class($context); } if (method_exists($class, $method)) { // insufficient visibility $rm = new \ReflectionMethod($class, $method); $visibility = $rm->isPrivate() ? 'private ' : ($rm->isProtected() ? 'protected ' : ''); throw new MemberAccessException("Call to {$visibility}method $class::$method() from " . ($context ? "scope $context." : 'global scope.')); } else { $hint = self::getSuggestion( array_filter((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC), fn($m) => $m->isStatic()), $method, ); throw new MemberAccessException("Call to undefined static method $class::$method()" . ($hint ? ", did you mean $hint()?" : '.')); } } /** * Returns array of magic properties defined by annotation @property. * @return array of [name => bit mask] * @internal */ public static function getMagicProperties(string $class): array { static $cache; $props = &$cache[$class]; if ($props !== null) { return $props; } $rc = new \ReflectionClass($class); preg_match_all( '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', (string) $rc->getDocComment(), $matches, PREG_SET_ORDER, ); $props = []; foreach ($matches as [, $type, $name]) { $uname = ucfirst($name); $write = $type !== '-read' && $rc->hasMethod($nm = 'set' . $uname) && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); $read = $type !== '-write' && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname)) && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); if ($read || $write) { $props[$name] = $read << 0 | ($nm[0] === 'g') << 1 | $rm->returnsReference() << 2 | $write << 3 | ($type === '-deprecated') << 4; } } foreach ($rc->getTraits() as $trait) { $props += self::getMagicProperties($trait->name); } if ($parent = get_parent_class($class)) { $props += self::getMagicProperties($parent); } return $props; } /** * Finds the best suggestion (for 8-bit encoding). * @param (\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionClass|\ReflectionProperty|string)[] $possibilities * @internal */ public static function getSuggestion(array $possibilities, string $value): ?string { $norm = preg_replace($re = '#^(get|set|has|is|add)(?=[A-Z])#', '+', $value); $best = null; $min = (strlen($value) / 4 + 1) * 10 + .1; foreach (array_unique($possibilities, SORT_REGULAR) as $item) { $item = $item instanceof \Reflector ? $item->name : $item; if ($item !== $value && ( ($len = levenshtein($item, $value, 10, 11, 10)) < $min || ($len = levenshtein(preg_replace($re, '*', $item), $norm, 10, 11, 10)) < $min )) { $min = $len; $best = $item; } } return $best; } private static function parseFullDoc(\ReflectionClass $rc, string $pattern): array { do { $doc[] = $rc->getDocComment(); $traits = $rc->getTraits(); while ($trait = array_pop($traits)) { $doc[] = $trait->getDocComment(); $traits += $trait->getTraits(); } } while ($rc = $rc->getParentClass()); return preg_match_all($pattern, implode('', $doc), $m) ? $m[1] : []; } /** * Checks if the public non-static property exists. * Returns 'event' if the property exists and has event like name * @internal */ public static function hasProperty(string $class, string $name): bool|string { static $cache; $prop = &$cache[$class][$name]; if ($prop === null) { $prop = false; try { $rp = new \ReflectionProperty($class, $name); if ($rp->isPublic() && !$rp->isStatic()) { $prop = $name >= 'onA' && $name < 'on_' ? 'event' : true; } } catch (\ReflectionException $e) { } } return $prop; } } Utils/Strings.php000064400000052715150250563100010015 0ustar00= 0xD800 && $code <= 0xDFFF) || $code > 0x10FFFF) { throw new Nette\InvalidArgumentException('Code point must be in range 0x0 to 0xD7FF or 0xE000 to 0x10FFFF.'); } elseif (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code)); } /** * Returns a code point of specific character in UTF-8 (number in range 0x0000..D7FF or 0xE000..10FFFF). */ public static function ord(string $c): int { if (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } $tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c); if (!$tmp) { throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".'); } return unpack('N', $tmp)[1]; } /** * @deprecated use str_starts_with() */ public static function startsWith(string $haystack, string $needle): bool { return str_starts_with($haystack, $needle); } /** * @deprecated use str_ends_with() */ public static function endsWith(string $haystack, string $needle): bool { return str_ends_with($haystack, $needle); } /** * @deprecated use str_contains() */ public static function contains(string $haystack, string $needle): bool { return str_contains($haystack, $needle); } /** * Returns a part of UTF-8 string specified by starting position and length. If start is negative, * the returned string will start at the start'th character from the end of string. */ public static function substring(string $s, int $start, ?int $length = null): string { if (function_exists('mb_substr')) { return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster } elseif (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires extension ICONV or MBSTRING, neither is loaded.'); } elseif ($length === null) { $length = self::length($s); } elseif ($start < 0 && $length < 0) { $start += self::length($s); // unifies iconv_substr behavior with mb_substr } return iconv_substr($s, $start, $length, 'UTF-8'); } /** * Removes control characters, normalizes line breaks to `\n`, removes leading and trailing blank lines, * trims end spaces on lines, normalizes UTF-8 to the normal form of NFC. */ public static function normalize(string $s): string { // convert to compressed normal form (NFC) if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) { $s = $n; } $s = self::unixNewLines($s); // remove control characters; leave \t + \n $s = self::pcre('preg_replace', ['#[\x00-\x08\x0B-\x1F\x7F-\x9F]+#u', '', $s]); // right trim $s = self::pcre('preg_replace', ['#[\t ]+$#m', '', $s]); // leading and trailing blank lines $s = trim($s, "\n"); return $s; } /** @deprecated use Strings::unixNewLines() */ public static function normalizeNewLines(string $s): string { return self::unixNewLines($s); } /** * Converts line endings to \n used on Unix-like systems. * Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator. */ public static function unixNewLines(string $s): string { return preg_replace("~\r\n?|\u{2028}|\u{2029}~", "\n", $s); } /** * Converts line endings to platform-specific, i.e. \r\n on Windows and \n elsewhere. * Line endings are: \n, \r, \r\n, U+2028 line separator, U+2029 paragraph separator. */ public static function platformNewLines(string $s): string { return preg_replace("~\r\n?|\n|\u{2028}|\u{2029}~", PHP_EOL, $s); } /** * Converts UTF-8 string to ASCII, ie removes diacritics etc. */ public static function toAscii(string $s): string { $iconv = defined('ICONV_IMPL') ? trim(ICONV_IMPL, '"\'') : null; static $transliterator = null; if ($transliterator === null) { if (class_exists('Transliterator', false)) { $transliterator = \Transliterator::create('Any-Latin; Latin-ASCII'); } else { trigger_error(__METHOD__ . "(): it is recommended to enable PHP extensions 'intl'.", E_USER_NOTICE); $transliterator = false; } } // remove control characters and check UTF-8 validity $s = self::pcre('preg_replace', ['#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{2FF}\x{370}-\x{10FFFF}]#u', '', $s]); // transliteration (by Transliterator and iconv) is not optimal, replace some characters directly $s = strtr($s, ["\u{201E}" => '"', "\u{201C}" => '"', "\u{201D}" => '"', "\u{201A}" => "'", "\u{2018}" => "'", "\u{2019}" => "'", "\u{B0}" => '^', "\u{42F}" => 'Ya', "\u{44F}" => 'ya', "\u{42E}" => 'Yu', "\u{44E}" => 'yu', "\u{c4}" => 'Ae', "\u{d6}" => 'Oe', "\u{dc}" => 'Ue', "\u{1e9e}" => 'Ss', "\u{e4}" => 'ae', "\u{f6}" => 'oe', "\u{fc}" => 'ue', "\u{df}" => 'ss']); // „ “ ” ‚ ‘ ’ ° Я я Ю ю Ä Ö Ü ẞ ä ö ü ß if ($iconv !== 'libiconv') { $s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ ` € ™ ℮ ← ↑ → ↓ ↔ } if ($transliterator) { $s = $transliterator->transliterate($s); // use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ if ($iconv === 'glibc') { $s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); $s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters } elseif ($iconv === 'libiconv') { $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } else { // null or 'unknown' (#216) $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars } } elseif ($iconv === 'glibc' || $iconv === 'libiconv') { // temporarily hide these characters to distinguish them from the garbage that iconv creates $s = strtr($s, '`\'"^~?', "\x01\x02\x03\x04\x05\x06"); if ($iconv === 'glibc') { // glibc implementation is very limited. transliterate into Windows-1250 and then into ASCII, so most Eastern European characters are preserved $s = iconv('UTF-8', 'WINDOWS-1250//TRANSLIT//IGNORE', $s); $s = strtr( $s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe\x96\xa0\x8b\x97\x9b\xa6\xad\xb7", 'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt- <->|-.', ); $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); } else { $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } // remove garbage that iconv creates during transliteration (eg Ý -> Y') $s = str_replace(['`', "'", '"', '^', '~', '?'], '', $s); // restore temporarily hidden characters $s = strtr($s, "\x01\x02\x03\x04\x05\x06", '`\'"^~?'); } else { $s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars } return $s; } /** * Modifies the UTF-8 string to the form used in the URL, ie removes diacritics and replaces all characters * except letters of the English alphabet and numbers with a hyphens. */ public static function webalize(string $s, ?string $charlist = null, bool $lower = true): string { $s = self::toAscii($s); if ($lower) { $s = strtolower($s); } $s = self::pcre('preg_replace', ['#[^a-z0-9' . ($charlist !== null ? preg_quote($charlist, '#') : '') . ']+#i', '-', $s]); $s = trim($s, '-'); return $s; } /** * Truncates a UTF-8 string to given maximal length, while trying not to split whole words. Only if the string is truncated, * an ellipsis (or something else set with third argument) is appended to the string. */ public static function truncate(string $s, int $maxLen, string $append = "\u{2026}"): string { if (self::length($s) > $maxLen) { $maxLen -= self::length($append); if ($maxLen < 1) { return $append; } elseif ($matches = self::match($s, '#^.{1,' . $maxLen . '}(?=[\s\x00-/:-@\[-`{-~])#us')) { return $matches[0] . $append; } else { return self::substring($s, 0, $maxLen) . $append; } } return $s; } /** * Indents a multiline text from the left. Second argument sets how many indentation chars should be used, * while the indent itself is the third argument (*tab* by default). */ public static function indent(string $s, int $level = 1, string $chars = "\t"): string { if ($level > 0) { $s = self::replace($s, '#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level)); } return $s; } /** * Converts all characters of UTF-8 string to lower case. */ public static function lower(string $s): string { return mb_strtolower($s, 'UTF-8'); } /** * Converts the first character of a UTF-8 string to lower case and leaves the other characters unchanged. */ public static function firstLower(string $s): string { return self::lower(self::substring($s, 0, 1)) . self::substring($s, 1); } /** * Converts all characters of a UTF-8 string to upper case. */ public static function upper(string $s): string { return mb_strtoupper($s, 'UTF-8'); } /** * Converts the first character of a UTF-8 string to upper case and leaves the other characters unchanged. */ public static function firstUpper(string $s): string { return self::upper(self::substring($s, 0, 1)) . self::substring($s, 1); } /** * Converts the first character of every word of a UTF-8 string to upper case and the others to lower case. */ public static function capitalize(string $s): string { return mb_convert_case($s, MB_CASE_TITLE, 'UTF-8'); } /** * Compares two UTF-8 strings or their parts, without taking character case into account. If length is null, whole strings are compared, * if it is negative, the corresponding number of characters from the end of the strings is compared, * otherwise the appropriate number of characters from the beginning is compared. */ public static function compare(string $left, string $right, ?int $length = null): bool { if (class_exists('Normalizer', false)) { $left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster $right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster } if ($length < 0) { $left = self::substring($left, $length, -$length); $right = self::substring($right, $length, -$length); } elseif ($length !== null) { $left = self::substring($left, 0, $length); $right = self::substring($right, 0, $length); } return self::lower($left) === self::lower($right); } /** * Finds the common prefix of strings or returns empty string if the prefix was not found. * @param string[] $strings */ public static function findPrefix(array $strings): string { $first = array_shift($strings); for ($i = 0; $i < strlen($first); $i++) { foreach ($strings as $s) { if (!isset($s[$i]) || $first[$i] !== $s[$i]) { while ($i && $first[$i - 1] >= "\x80" && $first[$i] >= "\x80" && $first[$i] < "\xC0") { $i--; } return substr($first, 0, $i); } } } return $first; } /** * Returns number of characters (not bytes) in UTF-8 string. * That is the number of Unicode code points which may differ from the number of graphemes. */ public static function length(string $s): int { return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s)); } /** * Removes all left and right side spaces (or the characters passed as second argument) from a UTF-8 encoded string. */ public static function trim(string $s, string $charlist = self::TrimCharacters): string { $charlist = preg_quote($charlist, '#'); return self::replace($s, '#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', ''); } /** * Pads a UTF-8 string to given length by prepending the $pad string to the beginning. */ public static function padLeft(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen) . $s; } /** * Pads UTF-8 string to given length by appending the $pad string to the end. */ public static function padRight(string $s, int $length, string $pad = ' '): string { $length = max(0, $length - self::length($s)); $padLen = self::length($pad); return $s . str_repeat($pad, (int) ($length / $padLen)) . self::substring($pad, 0, $length % $padLen); } /** * Reverses UTF-8 string. */ public static function reverse(string $s): string { if (!extension_loaded('iconv')) { throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.'); } return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s))); } /** * Returns part of $haystack before $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function before(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, 0, $pos); } /** * Returns part of $haystack after $nth occurence of $needle or returns null if the needle was not found. * Negative value means searching from the end. */ public static function after(string $haystack, string $needle, int $nth = 1): ?string { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : substr($haystack, $pos + strlen($needle)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the $needle was not found. * Negative value of `$nth` means searching from the end. */ public static function indexOf(string $haystack, string $needle, int $nth = 1): ?int { $pos = self::pos($haystack, $needle, $nth); return $pos === null ? null : self::length(substr($haystack, 0, $pos)); } /** * Returns position in characters of $nth occurence of $needle in $haystack or null if the needle was not found. */ private static function pos(string $haystack, string $needle, int $nth = 1): ?int { if (!$nth) { return null; } elseif ($nth > 0) { if ($needle === '') { return 0; } $pos = 0; while (($pos = strpos($haystack, $needle, $pos)) !== false && --$nth) { $pos++; } } else { $len = strlen($haystack); if ($needle === '') { return $len; } elseif ($len === 0) { return null; } $pos = $len - 1; while (($pos = strrpos($haystack, $needle, $pos - $len)) !== false && ++$nth) { $pos--; } } return Helpers::falseToNull($pos); } /** * Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well. */ public static function split( string $subject, #[Language('RegExp')] string $pattern, bool|int $captureOffset = false, bool $skipEmpty = false, int $limit = -1, bool $utf8 = false, ): array { $flags = is_int($captureOffset) // back compatibility ? $captureOffset : ($captureOffset ? PREG_SPLIT_OFFSET_CAPTURE : 0) | ($skipEmpty ? PREG_SPLIT_NO_EMPTY : 0); $pattern .= $utf8 ? 'u' : ''; $m = self::pcre('preg_split', [$pattern, $subject, $limit, $flags | PREG_SPLIT_DELIM_CAPTURE]); return $utf8 && $captureOffset ? self::bytesToChars($subject, [$m])[0] : $m; } /** * Searches the string for the part matching the regular expression and returns * an array with the found expression and individual subexpressions, or `null`. */ public static function match( string $subject, #[Language('RegExp')] string $pattern, bool|int $captureOffset = false, int $offset = 0, bool $unmatchedAsNull = false, bool $utf8 = false, ): ?array { $flags = is_int($captureOffset) // back compatibility ? $captureOffset : ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0); if ($utf8) { $offset = strlen(self::substring($subject, 0, $offset)); $pattern .= 'u'; } if ($offset > strlen($subject)) { return null; } elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) { return null; } elseif ($utf8 && $captureOffset) { return self::bytesToChars($subject, [$m])[0]; } else { return $m; } } /** * Searches the string for all occurrences matching the regular expression and * returns an array of arrays containing the found expression and each subexpression. */ public static function matchAll( string $subject, #[Language('RegExp')] string $pattern, bool|int $captureOffset = false, int $offset = 0, bool $unmatchedAsNull = false, bool $patternOrder = false, bool $utf8 = false, ): array { $flags = is_int($captureOffset) // back compatibility ? $captureOffset : ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0); if ($utf8) { $offset = strlen(self::substring($subject, 0, $offset)); $pattern .= 'u'; } if ($offset > strlen($subject)) { return []; } self::pcre('preg_match_all', [ $pattern, $subject, &$m, ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), $offset, ]); return $utf8 && $captureOffset ? self::bytesToChars($subject, $m) : $m; } /** * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`. */ public static function replace( string $subject, #[Language('RegExp')] string|array $pattern, string|callable $replacement = '', int $limit = -1, bool $captureOffset = false, bool $unmatchedAsNull = false, bool $utf8 = false, ): string { if (is_object($replacement) || is_array($replacement)) { if (!is_callable($replacement, false, $textual)) { throw new Nette\InvalidStateException("Callback '$textual' is not callable."); } $flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0); if ($utf8) { $pattern .= 'u'; if ($captureOffset) { $replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]); } } return self::pcre('preg_replace_callback', [$pattern, $replacement, $subject, $limit, 0, $flags]); } elseif (is_array($pattern) && is_string(key($pattern))) { $replacement = array_values($pattern); $pattern = array_keys($pattern); } if ($utf8) { $pattern = array_map(fn($item) => $item . 'u', (array) $pattern); } return self::pcre('preg_replace', [$pattern, $replacement, $subject, $limit]); } private static function bytesToChars(string $s, array $groups): array { $lastBytes = $lastChars = 0; foreach ($groups as &$matches) { foreach ($matches as &$match) { if ($match[1] > $lastBytes) { $lastChars += self::length(substr($s, $lastBytes, $match[1] - $lastBytes)); } elseif ($match[1] < $lastBytes) { $lastChars -= self::length(substr($s, $match[1], $lastBytes - $match[1])); } $lastBytes = $match[1]; $match[1] = $lastChars; } } return $groups; } /** @internal */ public static function pcre(string $func, array $args) { $res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void { // compile-time error, not detectable by preg_last_error throw new RegexpException($message . ' in pattern: ' . implode(' or ', (array) $args[0])); }); if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars && ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true)) ) { throw new RegexpException(preg_last_error_msg() . ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code); } return $res; } } Utils/FileInfo.php000064400000002413150250563100010045 0ustar00setInfoClass(static::class); $this->relativePath = $relativePath; } /** * Returns the relative directory path. */ public function getRelativePath(): string { return $this->relativePath; } /** * Returns the relative path including file name. */ public function getRelativePathname(): string { return ($this->relativePath === '' ? '' : $this->relativePath . DIRECTORY_SEPARATOR) . $this->getBasename(); } /** * Returns the contents of the file. * @throws Nette\IOException */ public function read(): string { return FileSystem::read($this->getPathname()); } /** * Writes the contents to the file. * @throws Nette\IOException */ public function write(string $content): void { FileSystem::write($this->getPathname(), $content); } } Utils/FileSystem.php000064400000022130150250563100010434 0ustar00getPathname()); } foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) { if ($item->isDir()) { static::createDir($target . '/' . $iterator->getSubPathName()); } else { static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName()); } } } else { static::createDir(dirname($target)); if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to copy file '%s' to '%s'. %s", self::normalizePath($origin), self::normalizePath($target), Helpers::getLastError(), )); } } } /** * Opens file and returns resource. * @return resource * @throws Nette\IOException on error occurred */ public static function open(string $path, string $mode) { $f = @fopen($path, $mode); // @ is escalated to exception if (!$f) { throw new Nette\IOException(sprintf( "Unable to open file '%s'. %s", self::normalizePath($path), Helpers::getLastError(), )); } return $f; } /** * Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first. * @throws Nette\IOException on error occurred */ public static function delete(string $path): void { if (is_file($path) || is_link($path)) { $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink'; if (!@$func($path)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to delete '%s'. %s", self::normalizePath($path), Helpers::getLastError(), )); } } elseif (is_dir($path)) { foreach (new \FilesystemIterator($path) as $item) { static::delete($item->getPathname()); } if (!@rmdir($path)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to delete directory '%s'. %s", self::normalizePath($path), Helpers::getLastError(), )); } } } /** * Renames or moves a file or a directory. Overwrites existing files and directories by default. * @throws Nette\IOException on error occurred * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists */ public static function rename(string $origin, string $target, bool $overwrite = true): void { if (!$overwrite && file_exists($target)) { throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target))); } elseif (!file_exists($origin)) { throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin))); } else { static::createDir(dirname($target)); if (realpath($origin) !== realpath($target)) { static::delete($target); } if (!@rename($origin, $target)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to rename file or directory '%s' to '%s'. %s", self::normalizePath($origin), self::normalizePath($target), Helpers::getLastError(), )); } } } /** * Reads the content of a file. * @throws Nette\IOException on error occurred */ public static function read(string $file): string { $content = @file_get_contents($file); // @ is escalated to exception if ($content === false) { throw new Nette\IOException(sprintf( "Unable to read file '%s'. %s", self::normalizePath($file), Helpers::getLastError(), )); } return $content; } /** * Reads the file content line by line. Because it reads continuously as we iterate over the lines, * it is possible to read files larger than the available memory. * @return \Generator * @throws Nette\IOException on error occurred */ public static function readLines(string $file, bool $stripNewLines = true): \Generator { return (function ($f) use ($file, $stripNewLines) { $counter = 0; do { $line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf( "Unable to read file '%s'. %s", self::normalizePath($file), $error, ))); if ($line === false) { fclose($f); break; } if ($stripNewLines) { $line = rtrim($line, "\r\n"); } yield $counter++ => $line; } while (true); })(static::open($file, 'r')); } /** * Writes the string to a file. * @throws Nette\IOException on error occurred */ public static function write(string $file, string $content, ?int $mode = 0666): void { static::createDir(dirname($file)); if (@file_put_contents($file, $content) === false) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to write file '%s'. %s", self::normalizePath($file), Helpers::getLastError(), )); } if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod file '%s' to mode %s. %s", self::normalizePath($file), decoct($mode), Helpers::getLastError(), )); } } /** * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`. * Recursively traverses and sets permissions on the entire contents of the directory as well. * @throws Nette\IOException on error occurred */ public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void { if (is_file($path)) { if (!@chmod($path, $fileMode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod file '%s' to mode %s. %s", self::normalizePath($path), decoct($fileMode), Helpers::getLastError(), )); } } elseif (is_dir($path)) { foreach (new \FilesystemIterator($path) as $item) { static::makeWritable($item->getPathname(), $dirMode, $fileMode); } if (!@chmod($path, $dirMode)) { // @ is escalated to exception throw new Nette\IOException(sprintf( "Unable to chmod directory '%s' to mode %s. %s", self::normalizePath($path), decoct($dirMode), Helpers::getLastError(), )); } } else { throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path))); } } /** * Determines if the path is absolute. */ public static function isAbsolute(string $path): bool { return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path); } /** * Normalizes `..` and `.` and directory separators in path. */ public static function normalizePath(string $path): string { $parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path); $res = []; foreach ($parts as $part) { if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') { array_pop($res); } elseif ($part !== '.') { $res[] = $part; } } return $res === [''] ? DIRECTORY_SEPARATOR : implode(DIRECTORY_SEPARATOR, $res); } /** * Joins all segments of the path and normalizes the result. */ public static function joinPaths(string ...$paths): string { return self::normalizePath(implode('/', $paths)); } /** * Converts backslashes to slashes. */ public static function unixSlashes(string $path): string { return strtr($path, '\\', '/'); } /** * Converts slashes to platform-specific directory separators. */ public static function platformSlashes(string $path): string { return DIRECTORY_SEPARATOR === '/' ? strtr($path, '\\', '/') : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol:// } } Utils/Random.php000064400000002113150250563100007567 0ustar00 implode('', range($m[0][0], $m[0][2])), $charlist, ); $charlist = count_chars($charlist, mode: 3); $chLen = strlen($charlist); if ($length < 1) { throw new Nette\InvalidArgumentException('Length must be greater than zero.'); } elseif ($chLen < 2) { throw new Nette\InvalidArgumentException('Character list must contain at least two chars.'); } $res = ''; for ($i = 0; $i < $length; $i++) { $res .= $charlist[random_int(0, $chLen - 1)]; } return $res; } } Utils/Callback.php000064400000006712150250563100010054 0ustar00name, '}')) { return $closure; } elseif ($obj = $r->getClosureThis()) { return [$obj, $r->name]; } elseif ($class = $r->getClosureScopeClass()) { return [$class->name, $r->name]; } else { return $r->name; } } } HtmlStringable.php000064400000000517150250563100010174 0ustar00getDefaultKind(); if (!\in_array($kind, static::getPossibleKinds())) { throw new \InvalidArgumentException('Unknown parser kind'); } $parser = $originalFactory->create(\constant(OriginalParserFactory::class.'::'.$kind)); return $parser; } } Sudo.php000064400000012441150250565140006174 0ustar00property */ public static function fetchProperty($object, string $property) { $prop = self::getProperty(new \ReflectionObject($object), $property); return $prop->getValue($object); } /** * Assign the value of a property of an object, bypassing visibility restrictions. * * @param object $object * @param string $property property name * @param mixed $value * * @return mixed Value of $object->property */ public static function assignProperty($object, string $property, $value) { $prop = self::getProperty(new \ReflectionObject($object), $property); $prop->setValue($object, $value); return $value; } /** * Call a method on an object, bypassing visibility restrictions. * * @param object $object * @param string $method method name * @param mixed $args... * * @return mixed */ public static function callMethod($object, string $method, ...$args) { $refl = new \ReflectionObject($object); $reflMethod = $refl->getMethod($method); $reflMethod->setAccessible(true); return $reflMethod->invokeArgs($object, $args); } /** * Fetch a property of a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $property property name * * @return mixed Value of $class::$property */ public static function fetchStaticProperty($class, string $property) { $prop = self::getProperty(new \ReflectionClass($class), $property); $prop->setAccessible(true); return $prop->getValue(); } /** * Assign the value of a static property of a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $property property name * @param mixed $value * * @return mixed Value of $class::$property */ public static function assignStaticProperty($class, string $property, $value) { $prop = self::getProperty(new \ReflectionClass($class), $property); $prop->setValue($value); return $value; } /** * Call a static method on a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $method method name * @param mixed $args... * * @return mixed */ public static function callStatic($class, string $method, ...$args) { $refl = new \ReflectionClass($class); $reflMethod = $refl->getMethod($method); $reflMethod->setAccessible(true); return $reflMethod->invokeArgs(null, $args); } /** * Fetch a class constant, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $const constant name * * @return mixed */ public static function fetchClassConst($class, string $const) { $refl = new \ReflectionClass($class); do { if ($refl->hasConstant($const)) { return $refl->getConstant($const); } $refl = $refl->getParentClass(); } while ($refl !== false); return false; } /** * Construct an instance of a class, bypassing private constructors. * * @param string $class class name * @param mixed $args... */ public static function newInstance(string $class, ...$args) { $refl = new \ReflectionClass($class); $instance = $refl->newInstanceWithoutConstructor(); $constructor = $refl->getConstructor(); $constructor->setAccessible(true); $constructor->invokeArgs($instance, $args); return $instance; } /** * Get a ReflectionProperty from an object (or its parent classes). * * @throws \ReflectionException if neither the object nor any of its parents has this property * * @param \ReflectionClass $refl * @param string $property property name * * @return \ReflectionProperty */ private static function getProperty(\ReflectionClass $refl, string $property): \ReflectionProperty { $firstException = null; do { try { $prop = $refl->getProperty($property); $prop->setAccessible(true); return $prop; } catch (\ReflectionException $e) { if ($firstException === null) { $firstException = $e; } $refl = $refl->getParentClass(); } } while ($refl !== false); throw $firstException; } } CodeCleaner.php000064400000027563150250565140007441 0ustar00yolo = $yolo; if ($parser === null) { $parserFactory = new ParserFactory(); $parser = $parserFactory->createParser(); } $this->parser = $parser; $this->printer = $printer ?: new Printer(); $this->traverser = $traverser ?: new NodeTraverser(); foreach ($this->getDefaultPasses() as $pass) { $this->traverser->addVisitor($pass); } } /** * Check whether this CodeCleaner is in YOLO mode. */ public function yolo(): bool { return $this->yolo; } /** * Get default CodeCleaner passes. * * @return CodeCleanerPass[] */ private function getDefaultPasses(): array { if ($this->yolo) { return $this->getYoloPasses(); } $useStatementPass = new UseStatementPass(); $namespacePass = new NamespacePass($this); // Try to add implicit `use` statements and an implicit namespace, // based on the file in which the `debug` call was made. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]); return [ // Validation passes new AbstractClassPass(), new AssignThisVariablePass(), new CalledClassPass(), new CallTimePassByReferencePass(), new FinalClassPass(), new FunctionContextPass(), new FunctionReturnInWriteContextPass(), new InstanceOfPass(), new IssetPass(), new LabelContextPass(), new LeavePsyshAlonePass(), new ListPass(), new LoopContextPass(), new PassableByReferencePass(), new ReturnTypePass(), new EmptyArrayDimFetchPass(), new ValidConstructorPass(), // Rewriting shenanigans $useStatementPass, // must run before the namespace pass new ExitPass(), new ImplicitReturnPass(), new MagicConstantsPass(), $namespacePass, // must run after the implicit return pass new RequirePass(), new StrictTypesPass(), // Namespace-aware validation (which depends on aforementioned shenanigans) new ValidClassNamePass(), new ValidFunctionNamePass(), ]; } /** * A set of code cleaner passes that don't try to do any validation, and * only do minimal rewriting to make things work inside the REPL. * * This list should stay in sync with the "rewriting shenanigans" in * getDefaultPasses above. * * @return CodeCleanerPass[] */ private function getYoloPasses(): array { $useStatementPass = new UseStatementPass(); $namespacePass = new NamespacePass($this); // Try to add implicit `use` statements and an implicit namespace, // based on the file in which the `debug` call was made. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]); return [ new LeavePsyshAlonePass(), $useStatementPass, // must run before the namespace pass new ExitPass(), new ImplicitReturnPass(), new MagicConstantsPass(), $namespacePass, // must run after the implicit return pass new RequirePass(), new StrictTypesPass(), ]; } /** * "Warm up" code cleaner passes when we're coming from a debug call. * * This is useful, for example, for `UseStatementPass` and `NamespacePass` * which keep track of state between calls, to maintain the current * namespace and a map of use statements. * * @param array $passes */ private function addImplicitDebugContext(array $passes) { $file = $this->getDebugFile(); if ($file === null) { return; } try { $code = @\file_get_contents($file); if (!$code) { return; } $stmts = $this->parse($code, true); if ($stmts === false) { return; } // Set up a clean traverser for just these code cleaner passes $traverser = new NodeTraverser(); foreach ($passes as $pass) { $traverser->addVisitor($pass); } $traverser->traverse($stmts); } catch (\Throwable $e) { // Don't care. } } /** * Search the stack trace for a file in which the user called Psy\debug. * * @return string|null */ private static function getDebugFile() { $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); foreach (\array_reverse($trace) as $stackFrame) { if (!self::isDebugCall($stackFrame)) { continue; } if (\preg_match('/eval\(/', $stackFrame['file'])) { \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches); return $matches[1][0]; } return $stackFrame['file']; } } /** * Check whether a given backtrace frame is a call to Psy\debug. * * @param array $stackFrame */ private static function isDebugCall(array $stackFrame): bool { $class = isset($stackFrame['class']) ? $stackFrame['class'] : null; $function = isset($stackFrame['function']) ? $stackFrame['function'] : null; return ($class === null && $function === 'Psy\\debug') || ($class === Shell::class && $function === 'debug'); } /** * Clean the given array of code. * * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP * * @param array $codeLines * @param bool $requireSemicolons * * @return string|false Cleaned PHP code, False if the input is incomplete */ public function clean(array $codeLines, bool $requireSemicolons = false) { $stmts = $this->parse('traverser->traverse($stmts); // Work around https://github.com/nikic/PHP-Parser/issues/399 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $code = $this->printer->prettyPrint($stmts); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); return $code; } /** * Set the current local namespace. * * @param array|null $namespace (default: null) */ public function setNamespace(array $namespace = null) { $this->namespace = $namespace; } /** * Get the current local namespace. * * @return array|null */ public function getNamespace() { return $this->namespace; } /** * Lex and parse a block of code. * * @see Parser::parse * * @throws ParseErrorException for parse errors that can't be resolved by * waiting a line to see what comes next * * @param string $code * @param bool $requireSemicolons * * @return array|false A set of statements, or false if incomplete */ protected function parse(string $code, bool $requireSemicolons = false) { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if ($this->parseErrorIsUnclosedString($e, $code)) { return false; } if ($this->parseErrorIsUnterminatedComment($e, $code)) { return false; } if ($this->parseErrorIsTrailingComma($e, $code)) { return false; } if (!$this->parseErrorIsEOF($e)) { throw ParseErrorException::fromParseError($e); } if ($requireSemicolons) { return false; } try { // Unexpected EOF, try again with an implicit semicolon return $this->parser->parse($code.';'); } catch (\PhpParser\Error $e) { return false; } } } private function parseErrorIsEOF(\PhpParser\Error $e): bool { $msg = $e->getRawMessage(); return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false); } /** * A special test for unclosed single-quoted strings. * * Unlike (all?) other unclosed statements, single quoted strings have * their own special beautiful snowflake syntax error just for * themselves. * * @param \PhpParser\Error $e * @param string $code */ private function parseErrorIsUnclosedString(\PhpParser\Error $e, string $code): bool { if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') { return false; } try { $this->parser->parse($code."';"); } catch (\Throwable $e) { return false; } return true; } private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code): bool { return $e->getRawMessage() === 'Unterminated comment'; } private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code): bool { return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ','); } } SuperglobalsEnv.php000064400000001154150250565140010374 0ustar00name = $name; } /** * Gets the constant name. * * @return string */ public function getName(): string { return $this->name; } /** * This can't (and shouldn't) do anything :). * * @throws \RuntimeException */ public static function export($name) { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * To string. * * @return string */ public function __toString(): string { return $this->getName(); } } Reflection/ReflectionLanguageConstruct.php000064400000006664150250565140015031 0ustar00 [ 'var' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'unset' => [ 'var' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'empty' => [ 'var' => [], ], 'echo' => [ 'arg1' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'print' => [ 'arg' => [], ], 'die' => [ 'status' => [ 'isOptional' => true, 'defaultValue' => 0, ], ], 'exit' => [ 'status' => [ 'isOptional' => true, 'defaultValue' => 0, ], ], ]; /** * Construct a ReflectionLanguageConstruct object. * * @param string $keyword */ public function __construct(string $keyword) { if (!self::isLanguageConstruct($keyword)) { throw new \InvalidArgumentException('Unknown language construct: '.$keyword); } $this->keyword = $keyword; } /** * This can't (and shouldn't) do anything :). * * @throws \RuntimeException */ public static function export($name) { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get language construct name. */ public function getName(): string { return $this->keyword; } /** * None of these return references. */ public function returnsReference(): bool { return false; } /** * Get language construct params. * * @return array */ public function getParameters(): array { $params = []; foreach (self::$languageConstructs[$this->keyword] as $parameter => $opts) { $params[] = new ReflectionLanguageConstructParameter($this->keyword, $parameter, $opts); } return $params; } /** * Gets the file name from a language construct. * * (Hint: it always returns false) * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.x (when we can use union types) * * @return string|false (false) */ #[\ReturnTypeWillChange] public function getFileName() { return false; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Check whether keyword is a (known) language construct. * * @param string $keyword */ public static function isLanguageConstruct(string $keyword): bool { return \array_key_exists($keyword, self::$languageConstructs); } } Reflection/ReflectionClassConstant.php000064400000012044150250565140014145 0ustar00class = $class; $this->name = $name; $constants = $class->getConstants(); if (!\array_key_exists($name, $constants)) { throw new \InvalidArgumentException('Unknown constant: '.$name); } $this->value = $constants[$name]; } /** * Exports a reflection. * * @param string|object $class * @param string $name * @param bool $return pass true to return the export, as opposed to emitting it * * @return string|null */ public static function export($class, string $name, bool $return = false) { $refl = new self($class, $name); $value = $refl->getValue(); $str = \sprintf('Constant [ public %s %s ] { %s }', \gettype($value), $refl->getName(), $value); if ($return) { return $str; } echo $str."\n"; } /** * Gets the declaring class. */ public function getDeclaringClass(): \ReflectionClass { $parent = $this->class; // Since we don't have real reflection constants, we can't see where // it's actually defined. Let's check for a constant that is also // available on the parent class which has exactly the same value. // // While this isn't _technically_ correct, it's prolly close enough. do { $class = $parent; $parent = $class->getParentClass(); } while ($parent && $parent->hasConstant($this->name) && $parent->getConstant($this->name) === $this->value); return $class; } /** * Get the constant's docblock. * * @return false */ public function getDocComment(): bool { return false; } /** * Gets the class constant modifiers. * * Since this is only used for PHP < 7.1, we can just return "public". All * the fancier modifiers are only available on PHP versions which have their * own ReflectionClassConstant class :) */ public function getModifiers(): int { return \ReflectionMethod::IS_PUBLIC; } /** * Gets the constant name. */ public function getName(): string { return $this->name; } /** * Gets the value of the constant. * * @return mixed */ public function getValue() { return $this->value; } /** * Checks if class constant is private. * * @return bool false */ public function isPrivate(): bool { return false; } /** * Checks if class constant is protected. * * @return bool false */ public function isProtected(): bool { return false; } /** * Checks if class constant is public. * * @return bool true */ public function isPublic(): bool { return true; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Gets the constant's file name. * * Currently returns null, because if it returns a file name the signature * formatter will barf. */ public function getFileName() { return; // return $this->class->getFileName(); } /** * Get the code start line. * * @throws \RuntimeException */ public function getStartLine() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get the code end line. * * @throws \RuntimeException */ public function getEndLine() { return $this->getStartLine(); } /** * Get a ReflectionClassConstant instance. * * In PHP >= 7.1, this will return a \ReflectionClassConstant from the * standard reflection library. For older PHP, it will return this polyfill. * * @param string|object $class * @param string $name * * @return ReflectionClassConstant|\ReflectionClassConstant */ public static function create($class, string $name) { if (\class_exists(\ReflectionClassConstant::class)) { return new \ReflectionClassConstant($class, $name); } return new self($class, $name); } } Reflection/ReflectionLanguageConstructParameter.php000064400000004725150250565140016666 0ustar00function = $function; $this->parameter = $parameter; $this->opts = $opts; } /** * No class here. * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.0 (when we can use nullable types) */ #[\ReturnTypeWillChange] public function getClass() { return; } /** * Is the param an array? * * @return bool */ public function isArray(): bool { return \array_key_exists('isArray', $this->opts) && $this->opts['isArray']; } /** * Get param default value. * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.x (when we can use mixed type) * * @return mixed */ #[\ReturnTypeWillChange] public function getDefaultValue() { if ($this->isDefaultValueAvailable()) { return $this->opts['defaultValue']; } return null; } /** * Get param name. * * @return string */ public function getName(): string { return $this->parameter; } /** * Is the param optional? * * @return bool */ public function isOptional(): bool { return \array_key_exists('isOptional', $this->opts) && $this->opts['isOptional']; } /** * Does the param have a default value? * * @return bool */ public function isDefaultValueAvailable(): bool { return \array_key_exists('defaultValue', $this->opts); } /** * Is the param passed by reference? * * (I don't think this is true for anything we need to fake a param for) * * @return bool */ public function isPassedByReference(): bool { return \array_key_exists('isPassedByReference', $this->opts) && $this->opts['isPassedByReference']; } } Reflection/ReflectionConstant_.php000064400000007171150250565140013323 0ustar00name = $name; if (!\defined($name) && !self::isMagicConstant($name)) { throw new \InvalidArgumentException('Unknown constant: '.$name); } if (!self::isMagicConstant($name)) { $this->value = @\constant($name); } } /** * Exports a reflection. * * @param string $name * @param bool $return pass true to return the export, as opposed to emitting it * * @return string|null */ public static function export(string $name, bool $return = false) { $refl = new self($name); $value = $refl->getValue(); $str = \sprintf('Constant [ %s %s ] { %s }', \gettype($value), $refl->getName(), $value); if ($return) { return $str; } echo $str."\n"; } public static function isMagicConstant($name) { return \in_array($name, self::$magicConstants); } /** * Get the constant's docblock. * * @return false */ public function getDocComment(): bool { return false; } /** * Gets the constant name. */ public function getName(): string { return $this->name; } /** * Gets the namespace name. * * Returns '' when the constant is not namespaced. */ public function getNamespaceName(): string { if (!$this->inNamespace()) { return ''; } return \preg_replace('/\\\\[^\\\\]+$/', '', $this->name); } /** * Gets the value of the constant. * * @return mixed */ public function getValue() { return $this->value; } /** * Checks if this constant is defined in a namespace. */ public function inNamespace(): bool { return \strpos($this->name, '\\') !== false; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Gets the constant's file name. * * Currently returns null, because if it returns a file name the signature * formatter will barf. */ public function getFileName() { return; // return $this->class->getFileName(); } /** * Get the code start line. * * @throws \RuntimeException */ public function getStartLine() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get the code end line. * * @throws \RuntimeException */ public function getEndLine() { return $this->getStartLine(); } } Sudo/SudoVisitor.php000064400000013214150250565140010465 0ustar00name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ $node->var, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::PROPERTY_FETCH, $args); } elseif ($node instanceof Assign && $node->var instanceof PropertyFetch) { $target = $node->var; $name = $target->name instanceof Identifier ? $target->name->toString() : $target->name; $args = [ $target->var, \is_string($name) ? new String_($name) : $name, $node->expr, ]; return $this->prepareCall(self::PROPERTY_ASSIGN, $args); } elseif ($node instanceof MethodCall) { $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = $node->args; \array_unshift($args, new Arg(\is_string($name) ? new String_($name) : $name)); \array_unshift($args, new Arg($node->var)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::METHOD_CALL, $args); } elseif ($node instanceof StaticPropertyFetch) { $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::STATIC_PROPERTY_FETCH, $args); } elseif ($node instanceof Assign && $node->var instanceof StaticPropertyFetch) { $target = $node->var; $class = $target->class instanceof Name ? $target->class->toString() : $target->class; $name = $target->name instanceof Identifier ? $target->name->toString() : $target->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, $node->expr, ]; return $this->prepareCall(self::STATIC_PROPERTY_ASSIGN, $args); } elseif ($node instanceof StaticCall) { $args = $node->args; $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; \array_unshift($args, new Arg(\is_string($name) ? new String_($name) : $name)); \array_unshift($args, new Arg(\is_string($class) ? new String_($class) : $class)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::STATIC_CALL, $args); } elseif ($node instanceof ClassConstFetch) { $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::CLASS_CONST_FETCH, $args); } elseif ($node instanceof New_) { $args = $node->args; $class = $node->class instanceof Name ? $node->class->toString() : $node->class; \array_unshift($args, new Arg(\is_string($class) ? new String_($class) : $class)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::NEW_INSTANCE, $args); } } private function prepareCall(string $method, array $args): StaticCall { return new StaticCall(new FullyQualifiedName(Sudo::class), $method, \array_map(function ($arg) { return new Arg($arg); }, $args)); } } ExecutionClosure.php000064400000004373150250565140010567 0ustar00setClosure($__psysh__, function () use ($__psysh__) { try { // Restore execution scope variables \extract($__psysh__->getScopeVariables(false)); // Buffer stdout; we'll need it later \ob_start([$__psysh__, 'writeStdout'], 1); // Convert all errors to exceptions \set_error_handler([$__psysh__, 'handleError']); // Evaluate the current code buffer $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: self::NOOP_INPUT)); } catch (\Throwable $_e) { // Clean up on our way out. if (\ob_get_level() > 0) { \ob_end_clean(); } throw $_e; } finally { // Won't be needing this anymore \restore_error_handler(); } // Flush stdout (write to shell output, plus save to magic variable) \ob_end_flush(); // Save execution scope variables for next time $__psysh__->setScopeVariables(\get_defined_vars()); return $_; }); } /** * Set the closure instance. * * @param Shell $shell * @param \Closure $closure */ protected function setClosure(Shell $shell, \Closure $closure) { $that = $shell->getBoundObject(); if (\is_object($that)) { $this->closure = $closure->bindTo($that, \get_class($that)); } else { $this->closure = $closure->bindTo(null, $shell->getBoundClass()); } } /** * Go go gadget closure. * * @return mixed */ public function execute() { $closure = $this->closure; return $closure(); } } Util/Docblock.php000064400000015226150250565140007723 0ustar00 * @author Justin Hileman */ class Docblock { /** * Tags in the docblock that have a whitespace-delimited number of parameters * (such as `@param type var desc` and `@return type desc`) and the names of * those parameters. * * @var array */ public static $vectors = [ 'throws' => ['type', 'desc'], 'param' => ['type', 'var', 'desc'], 'return' => ['type', 'desc'], ]; protected $reflector; /** * The description of the symbol. * * @var string */ public $desc; /** * The tags defined in the docblock. * * The array has keys which are the tag names (excluding the @) and values * that are arrays, each of which is an entry for the tag. * * In the case where the tag name is defined in {@see DocBlock::$vectors} the * value within the tag-value array is an array in itself with keys as * described by {@see DocBlock::$vectors}. * * @var array */ public $tags; /** * The entire DocBlock comment that was parsed. * * @var string */ public $comment; /** * Docblock constructor. * * @param \Reflector $reflector */ public function __construct(\Reflector $reflector) { $this->reflector = $reflector; if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionClassConstant || $reflector instanceof \ReflectionFunctionAbstract || $reflector instanceof \ReflectionProperty) { $this->setComment($reflector->getDocComment()); } } /** * Set and parse the docblock comment. * * @param string $comment The docblock */ protected function setComment(string $comment) { $this->desc = ''; $this->tags = []; $this->comment = $comment; $this->parseComment($comment); } /** * Find the length of the docblock prefix. * * @param array $lines * * @return int Prefix length */ protected static function prefixLength(array $lines): int { // find only lines with interesting things $lines = \array_filter($lines, function ($line) { return \substr($line, \strspn($line, "* \t\n\r\0\x0B")); }); // if we sort the lines, we only have to compare two items \sort($lines); $first = \reset($lines); $last = \end($lines); // Special case for single-line comments if (\count($lines) === 1) { return \strspn($first, "* \t\n\r\0\x0B"); } // find the longest common substring $count = \min(\strlen($first), \strlen($last)); for ($i = 0; $i < $count; $i++) { if ($first[$i] !== $last[$i]) { return $i; } } return $count; } /** * Parse the comment into the component parts and set the state of the object. * * @param string $comment The docblock */ protected function parseComment(string $comment) { // Strip the opening and closing tags of the docblock $comment = \substr($comment, 3, -2); // Split into arrays of lines $comment = \array_filter(\preg_split('/\r?\n\r?/', $comment)); // Trim asterisks and whitespace from the beginning and whitespace from the end of lines $prefixLength = self::prefixLength($comment); $comment = \array_map(function ($line) use ($prefixLength) { return \rtrim(\substr($line, $prefixLength)); }, $comment); // Group the lines together by @tags $blocks = []; $b = -1; foreach ($comment as $line) { if (self::isTagged($line)) { $b++; $blocks[] = []; } elseif ($b === -1) { $b = 0; $blocks[] = []; } $blocks[$b][] = $line; } // Parse the blocks foreach ($blocks as $block => $body) { $body = \trim(\implode("\n", $body)); if ($block === 0 && !self::isTagged($body)) { // This is the description block $this->desc = $body; } else { // This block is tagged $tag = \substr(self::strTag($body), 1); $body = \ltrim(\substr($body, \strlen($tag) + 2)); if (isset(self::$vectors[$tag])) { // The tagged block is a vector $count = \count(self::$vectors[$tag]); if ($body) { $parts = \preg_split('/\s+/', $body, $count); } else { $parts = []; } // Default the trailing values $parts = \array_pad($parts, $count, null); // Store as a mapped array $this->tags[$tag][] = \array_combine(self::$vectors[$tag], $parts); } else { // The tagged block is only text $this->tags[$tag][] = $body; } } } } /** * Whether or not a docblock contains a given @tag. * * @param string $tag The name of the @tag to check for */ public function hasTag(string $tag): bool { return \is_array($this->tags) && \array_key_exists($tag, $this->tags); } /** * The value of a tag. * * @param string $tag * * @return array|null */ public function tag(string $tag) { // TODO: Add proper null-type return values once the lowest PHP version supported is 7.1 return $this->hasTag($tag) ? $this->tags[$tag] : null; } /** * Whether or not a string begins with a @tag. * * @param string $str */ public static function isTagged(string $str): bool { return isset($str[1]) && $str[0] === '@' && !\preg_match('/[^A-Za-z]/', $str[1]); } /** * The tag at the beginning of a string. * * @param string $str * * @return string|null */ public static function strTag(string $str) { if (\preg_match('/^@[a-z0-9_]+/', $str, $matches)) { return $matches[0]; } } } Util/Str.php000064400000005614150250565140006753 0ustar00hasConstant($member)) { return ReflectionClassConstant::create($value, $member); } elseif ($filter & self::METHOD && $class->hasMethod($member)) { return $class->getMethod($member); } elseif ($filter & self::PROPERTY && $class->hasProperty($member)) { return $class->getProperty($member); } elseif ($filter & self::STATIC_PROPERTY && $class->hasProperty($member) && $class->getProperty($member)->isStatic()) { return $class->getProperty($member); } else { throw new RuntimeException(\sprintf('Unknown member %s on class %s', $member, \is_object($value) ? \get_class($value) : $value)); } } /** * Get a ReflectionClass (or ReflectionObject, or ReflectionNamespace) if possible. * * @throws \InvalidArgumentException if $value is not a namespace or class name or instance * * @param mixed $value * * @return \ReflectionClass|ReflectionNamespace */ private static function getClass($value) { if (\is_object($value)) { return new \ReflectionObject($value); } if (!\is_string($value)) { throw new \InvalidArgumentException('Mirror expects an object or class'); } if (\class_exists($value) || \interface_exists($value) || \trait_exists($value)) { return new \ReflectionClass($value); } $namespace = \preg_replace('/(^\\\\|\\\\$)/', '', $value); if (self::namespaceExists($namespace)) { return new ReflectionNamespace($namespace); } throw new \InvalidArgumentException('Unknown namespace, class or function: '.$value); } /** * Check declared namespaces for a given namespace. */ private static function namespaceExists(string $value): bool { return \in_array(\strtolower($value), self::getDeclaredNamespaces()); } /** * Get an array of all currently declared namespaces. * * Note that this relies on at least one function, class, interface, trait * or constant to have been declared in that namespace. */ private static function getDeclaredNamespaces(): array { $functions = \get_defined_functions(); $allNames = \array_merge( $functions['internal'], $functions['user'], \get_declared_classes(), \get_declared_interfaces(), \get_declared_traits(), \array_keys(\get_defined_constants()) ); $namespaces = []; foreach ($allNames as $name) { $chunks = \explode('\\', \strtolower($name)); // the last one is the function or class or whatever... \array_pop($chunks); while (!empty($chunks)) { $namespaces[\implode('\\', $chunks)] = true; \array_pop($chunks); } } $namespaceNames = \array_keys($namespaces); \sort($namespaceNames); return $namespaceNames; } } Context.php000064400000017334150250565140006714 0ustar00returnValue; case '_e': if (isset($this->lastException)) { return $this->lastException; } break; case '__out': if (isset($this->lastStdout)) { return $this->lastStdout; } break; case 'this': if (isset($this->boundObject)) { return $this->boundObject; } break; case '__function': case '__method': case '__class': case '__namespace': case '__file': case '__line': case '__dir': if (\array_key_exists($name, $this->commandScopeVariables)) { return $this->commandScopeVariables[$name]; } break; default: if (\array_key_exists($name, $this->scopeVariables)) { return $this->scopeVariables[$name]; } break; } throw new \InvalidArgumentException('Unknown variable: $'.$name); } /** * Get all defined variables. */ public function getAll(): array { return \array_merge($this->scopeVariables, $this->getSpecialVariables()); } /** * Get all defined magic variables: $_, $_e, $__out, $__class, $__file, etc. */ public function getSpecialVariables(): array { $vars = [ '_' => $this->returnValue, ]; if (isset($this->lastException)) { $vars['_e'] = $this->lastException; } if (isset($this->lastStdout)) { $vars['__out'] = $this->lastStdout; } if (isset($this->boundObject)) { $vars['this'] = $this->boundObject; } return \array_merge($vars, $this->commandScopeVariables); } /** * Set all scope variables. * * This method does *not* set any of the magic variables: $_, $_e, $__out, * $__class, $__file, etc. * * @param array $vars */ public function setAll(array $vars) { foreach (self::$specialNames as $key) { unset($vars[$key]); } foreach (self::$commandScopeNames as $key) { unset($vars[$key]); } $this->scopeVariables = $vars; } /** * Set the most recent return value. * * @param mixed $value */ public function setReturnValue($value) { $this->returnValue = $value; } /** * Get the most recent return value. * * @return mixed */ public function getReturnValue() { return $this->returnValue; } /** * Set the most recent Exception or Error. * * @param \Throwable $e */ public function setLastException(\Throwable $e) { $this->lastException = $e; } /** * Get the most recent Exception or Error. * * @throws \InvalidArgumentException If no Exception has been caught * * @return \Throwable|null */ public function getLastException() { if (!isset($this->lastException)) { throw new \InvalidArgumentException('No most-recent exception'); } return $this->lastException; } /** * Set the most recent output from evaluated code. * * @param string $lastStdout */ public function setLastStdout(string $lastStdout) { $this->lastStdout = $lastStdout; } /** * Get the most recent output from evaluated code. * * @throws \InvalidArgumentException If no output has happened yet * * @return string|null */ public function getLastStdout() { if (!isset($this->lastStdout)) { throw new \InvalidArgumentException('No most-recent output'); } return $this->lastStdout; } /** * Set the bound object ($this variable) for the interactive shell. * * Note that this unsets the bound class, if any exists. * * @param object|null $boundObject */ public function setBoundObject($boundObject) { $this->boundObject = \is_object($boundObject) ? $boundObject : null; $this->boundClass = null; } /** * Get the bound object ($this variable) for the interactive shell. * * @return object|null */ public function getBoundObject() { return $this->boundObject; } /** * Set the bound class (self) for the interactive shell. * * Note that this unsets the bound object, if any exists. * * @param string|null $boundClass */ public function setBoundClass($boundClass) { $this->boundClass = (\is_string($boundClass) && $boundClass !== '') ? $boundClass : null; $this->boundObject = null; } /** * Get the bound class (self) for the interactive shell. * * @return string|null */ public function getBoundClass() { return $this->boundClass; } /** * Set command-scope magic variables: $__class, $__file, etc. * * @param array $commandScopeVariables */ public function setCommandScopeVariables(array $commandScopeVariables) { $vars = []; foreach ($commandScopeVariables as $key => $value) { // kind of type check if (\is_scalar($value) && \in_array($key, self::$commandScopeNames)) { $vars[$key] = $value; } } $this->commandScopeVariables = $vars; } /** * Get command-scope magic variables: $__class, $__file, etc. */ public function getCommandScopeVariables(): array { return $this->commandScopeVariables; } /** * Get unused command-scope magic variables names: __class, __file, etc. * * This is used by the shell to unset old command-scope variables after a * new batch is set. * * @return array Array of unused variable names */ public function getUnusedCommandScopeVariableNames(): array { return \array_diff(self::$commandScopeNames, \array_keys($this->commandScopeVariables)); } /** * Check whether a variable name is a magic variable. * * @param string $name */ public static function isSpecialVariableName(string $name): bool { return \in_array($name, self::$specialNames) || \in_array($name, self::$commandScopeNames); } } Output/ShellOutput.php000064400000013355150250565140011057 0ustar00theme = $theme ?? new Theme('modern'); $this->initFormatters(); if ($pager === null) { $this->pager = new PassthruPager($this); } elseif (\is_string($pager)) { $this->pager = new ProcOutputPager($this, $pager); } elseif ($pager instanceof OutputPager) { $this->pager = $pager; } else { throw new \InvalidArgumentException('Unexpected pager parameter: '.$pager); } } /** * Page multiple lines of output. * * The output pager is started * * If $messages is callable, it will be called, passing this output instance * for rendering. Otherwise, all passed $messages are paged to output. * * Upon completion, the output pager is flushed. * * @param string|array|\Closure $messages A string, array of strings or a callback * @param int $type (default: 0) */ public function page($messages, int $type = 0) { if (\is_string($messages)) { $messages = (array) $messages; } if (!\is_array($messages) && !\is_callable($messages)) { throw new \InvalidArgumentException('Paged output requires a string, array or callback'); } $this->startPaging(); if (\is_callable($messages)) { $messages($this); } else { $this->write($messages, true, $type); } $this->stopPaging(); } /** * Start sending output to the output pager. */ public function startPaging() { $this->paging++; } /** * Stop paging output and flush the output pager. */ public function stopPaging() { $this->paging--; $this->closePager(); } /** * Writes a message to the output. * * Optionally, pass `$type | self::NUMBER_LINES` as the $type parameter to * number the lines of output. * * @throws \InvalidArgumentException When unknown output type is given * * @param string|array $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $type The type of output */ public function write($messages, $newline = false, $type = 0) { if ($this->getVerbosity() === self::VERBOSITY_QUIET) { return; } $messages = (array) $messages; if ($type & self::NUMBER_LINES) { $pad = \strlen((string) \count($messages)); $template = $this->isDecorated() ? ": %s" : "%{$pad}s: %s"; if ($type & self::OUTPUT_RAW) { $messages = \array_map([OutputFormatter::class, 'escape'], $messages); } foreach ($messages as $i => $line) { $messages[$i] = \sprintf($template, $i, $line); } // clean this up for super. $type = $type & ~self::NUMBER_LINES & ~self::OUTPUT_RAW; } parent::write($messages, $newline, $type); } /** * Writes a message to the output. * * Handles paged output, or writes directly to the output stream. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not */ public function doWrite($message, $newline) { if ($this->paging > 0) { $this->pager->doWrite($message, $newline); } else { parent::doWrite($message, $newline); } } /** * Set the output Theme. */ public function setTheme(Theme $theme) { $this->theme = $theme; $this->initFormatters(); } /** * Flush and close the output pager. */ private function closePager() { if ($this->paging <= 0) { $this->pager->close(); } } /** * Initialize output formatter styles. */ private function initFormatters() { $useGrayFallback = !$this->grayExists(); $this->theme->applyStyles($this->getFormatter(), $useGrayFallback); $this->theme->applyErrorStyles($this->getErrorOutput()->getFormatter(), $useGrayFallback); } /** * Checks if the "gray" color exists on the output. */ private function grayExists(): bool { try { $this->write(''); } catch (\InvalidArgumentException $e) { return false; } return true; } } Output/OutputPager.php000064400000001066150250565140011042 0ustar00 true, ]; const CLASSIC_THEME = [ 'compact' => true, 'prompt' => '>>> ', 'bufferPrompt' => '... ', 'replayPrompt' => '--> ', 'returnValue' => '=> ', ]; const DEFAULT_STYLES = [ 'info' => ['white', 'blue', ['bold']], 'warning' => ['black', 'yellow'], 'error' => ['white', 'red', ['bold']], 'whisper' => ['gray'], 'aside' => ['blue'], 'strong' => [null, null, ['bold']], 'return' => ['cyan'], 'urgent' => ['red'], 'hidden' => ['black'], // Visibility 'public' => [null, null, ['bold']], 'protected' => ['yellow'], 'private' => ['red'], 'global' => ['cyan', null, ['bold']], 'const' => ['cyan'], 'class' => ['blue', null, ['underscore']], 'function' => [null], 'default' => [null], // Types 'number' => ['magenta'], 'integer' => ['magenta'], 'float' => ['yellow'], 'string' => ['green'], 'bool' => ['cyan'], 'keyword' => ['yellow'], 'comment' => ['blue'], 'object' => ['blue'], 'resource' => ['yellow'], // Code-specific formatting 'inline_html' => ['cyan'], ]; const ERROR_STYLES = ['info', 'warning', 'error', 'whisper']; private $compact = false; private $prompt = '> '; private $bufferPrompt = '. '; private $replayPrompt = '- '; private $returnValue = '= '; private $grayFallback = 'blue'; private $styles = []; /** * @param string|array $config theme name or config options */ public function __construct($config = 'modern') { if (\is_string($config)) { switch ($config) { case 'modern': $config = static::MODERN_THEME; break; case 'compact': $config = static::COMPACT_THEME; break; case 'classic': $config = static::CLASSIC_THEME; break; default: \trigger_error(\sprintf('Unknown theme: %s', $config), \E_USER_NOTICE); $config = static::MODERN_THEME; break; } } if (!\is_array($config)) { throw new \InvalidArgumentException('Invalid theme config'); } foreach ($config as $name => $value) { switch ($name) { case 'compact': $this->setCompact($value); break; case 'prompt': $this->setPrompt($value); break; case 'bufferPrompt': $this->setBufferPrompt($value); break; case 'replayPrompt': $this->setReplayPrompt($value); break; case 'returnValue': $this->setReturnValue($value); break; case 'grayFallback': $this->setGrayFallback($value); break; } } $this->setStyles($config['styles'] ?? []); } /** * Enable or disable compact output. */ public function setCompact(bool $compact) { $this->compact = $compact; } /** * Get whether to use compact output. */ public function compact(): bool { return $this->compact; } /** * Set the prompt string. */ public function setPrompt(string $prompt) { $this->prompt = $prompt; } /** * Get the prompt string. */ public function prompt(): string { return $this->prompt; } /** * Set the buffer prompt string (used for multi-line input continuation). */ public function setBufferPrompt(string $bufferPrompt) { $this->bufferPrompt = $bufferPrompt; } /** * Get the buffer prompt string (used for multi-line input continuation). */ public function bufferPrompt(): string { return $this->bufferPrompt; } /** * Set the prompt string used when replaying history. */ public function setReplayPrompt(string $replayPrompt) { $this->replayPrompt = $replayPrompt; } /** * Get the prompt string used when replaying history. */ public function replayPrompt(): string { return $this->replayPrompt; } /** * Set the return value marker. */ public function setReturnValue(string $returnValue) { $this->returnValue = $returnValue; } /** * Get the return value marker. */ public function returnValue(): string { return $this->returnValue; } /** * Set the fallback color when "gray" is unavailable. */ public function setGrayFallback(string $grayFallback) { $this->grayFallback = $grayFallback; } /** * Set the shell output formatter styles. * * Accepts a map from style name to [fg, bg, options], for example: * * [ * 'error' => ['white', 'red', ['bold']], * 'warning' => ['black', 'yellow'], * ] * * Foreground, background or options can be null, or even omitted entirely. */ public function setStyles(array $styles) { foreach (\array_keys(static::DEFAULT_STYLES) as $name) { $this->styles[$name] = $styles[$name] ?? static::DEFAULT_STYLES[$name]; } } /** * Apply the current output formatter styles. */ public function applyStyles(OutputFormatterInterface $formatter, bool $useGrayFallback) { foreach (\array_keys(static::DEFAULT_STYLES) as $name) { $formatter->setStyle($name, new OutputFormatterStyle(...$this->getStyle($name, $useGrayFallback))); } } /** * Apply the current output formatter error styles. */ public function applyErrorStyles(OutputFormatterInterface $errorFormatter, bool $useGrayFallback) { foreach (static::ERROR_STYLES as $name) { $errorFormatter->setStyle($name, new OutputFormatterStyle(...$this->getStyle($name, $useGrayFallback))); } } private function getStyle(string $name, bool $useGrayFallback): array { return \array_map(function ($style) use ($useGrayFallback) { return ($useGrayFallback && $style === 'gray') ? $this->grayFallback : $style; }, $this->styles[$name]); } } Output/PassthruPager.php000064400000001451150250565140011351 0ustar00getStream()); } /** * Close the current pager process. */ public function close() { // nothing to do here } } Output/ProcOutputPager.php000064400000005435150250565140011672 0ustar00stream = $output->getStream(); $this->cmd = $cmd; } /** * Writes a message to the output. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not * * @throws \RuntimeException When unable to write output (should never happen) */ public function doWrite($message, $newline) { $pipe = $this->getPipe(); if (false === @\fwrite($pipe, $message.($newline ? \PHP_EOL : ''))) { // @codeCoverageIgnoreStart // should never happen $this->close(); throw new \RuntimeException('Unable to write output'); // @codeCoverageIgnoreEnd } \fflush($pipe); } /** * Close the current pager process. */ public function close() { if (isset($this->pipe)) { \fclose($this->pipe); } if (isset($this->proc)) { $exit = \proc_close($this->proc); if ($exit !== 0) { throw new \RuntimeException('Error closing output stream'); } } $this->pipe = null; $this->proc = null; } /** * Get a pipe for paging output. * * If no active pager process exists, fork one and return its input pipe. */ private function getPipe() { if (!isset($this->pipe) || !isset($this->proc)) { $desc = [['pipe', 'r'], $this->stream, \fopen('php://stderr', 'w')]; $this->proc = \proc_open($this->cmd, $desc, $pipes); if (!\is_resource($this->proc)) { throw new \RuntimeException('Error opening output stream'); } $this->pipe = $pipes[0]; } return $this->pipe; } } Exception/BreakException.php000064400000002106150250565140012120 0ustar00rawMessage = $message; parent::__construct(\sprintf('Exit: %s', $message), $code, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Throws BreakException. * * Since `throw` can not be inserted into arbitrary expressions, it wraps with function call. * * @throws BreakException */ public static function exitShell() { throw new self('Goodbye'); } } Exception/ThrowUpException.php000064400000002623150250565140012510 0ustar00getMessage()); parent::__construct($message, $throwable->getCode(), $throwable); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->getPrevious()->getMessage(); } /** * Create a ThrowUpException from a Throwable. * * @deprecated psySH no longer wraps Throwables * * @param \Throwable $throwable */ public static function fromThrowable($throwable): self { if ($throwable instanceof \Error) { $throwable = ErrorException::fromError($throwable); } if (!$throwable instanceof \Exception) { throw new \InvalidArgumentException('throw-up can only throw Exceptions and Errors'); } return new self($throwable); } } Exception/TypeErrorException.php000064400000002627150250565140013037 0ustar00rawMessage = $message; $message = \preg_replace('/, called in .*?: eval\\(\\)\'d code/', '', $message); parent::__construct(\sprintf('TypeError: %s', $message), $code, $previous); } /** * Get the raw (unformatted) message for this error. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Create a TypeErrorException from a TypeError. * * @deprecated psySH no longer wraps TypeErrors * * @param \TypeError $e */ public static function fromTypeError(\TypeError $e): self { return new self($e->getMessage(), $e->getCode(), $e); } } Exception/ErrorException.php000064400000006014150250565140012167 0ustar00rawMessage = $message; if (!empty($filename) && \preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) { $filename = ''; } switch ($severity) { case \E_STRICT: $type = 'Strict error'; break; case \E_NOTICE: case \E_USER_NOTICE: $type = 'Notice'; break; case \E_WARNING: case \E_CORE_WARNING: case \E_COMPILE_WARNING: case \E_USER_WARNING: $type = 'Warning'; break; case \E_DEPRECATED: case \E_USER_DEPRECATED: $type = 'Deprecated'; break; case \E_RECOVERABLE_ERROR: $type = 'Recoverable fatal error'; break; default: $type = 'Error'; break; } $message = \sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in '.$filename : '', $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Get the raw (unformatted) message for this error. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler([ErrorException::class, 'throwException']); * * @throws self * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public static function throwException($errno, $errstr, $errfile, $errline) { throw new self($errstr, 0, $errno, $errfile, $errline); } /** * Create an ErrorException from an Error. * * @deprecated psySH no longer wraps Errors * * @param \Error $e */ public static function fromError(\Error $e): self { return new self($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e); } } Exception/FatalErrorException.php000064400000002725150250565140013144 0ustar00rawMessage = $message; $message = \sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } } Exception/UnexpectedTargetException.php000064400000001536150250565140014355 0ustar00target = $target; parent::__construct($message, $code, $previous); } /** * @return mixed */ public function getTarget() { return $this->target; } } Exception/DeprecatedException.php000064400000000576150250565140013145 0ustar00getRawMessage(), $e->getStartLine()); } } Exception/RuntimeException.php000064400000001710150250565140012517 0ustar00rawMessage = $message; parent::__construct($message, $code, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } } ExecutionLoop/ProcessForker.php000064400000020473150250565140012652 0ustar00 0) { // This is the main thread. We'll just wait for a while. // We won't be needing this one. \fclose($up); // Wait for a return value from the loop process. $read = [$down]; $write = null; $except = null; do { $n = @\stream_select($read, $write, $except, null); if ($n === 0) { throw new \RuntimeException('Process timed out waiting for execution loop'); } if ($n === false) { $err = \error_get_last(); if (!isset($err['message']) || \stripos($err['message'], 'interrupted system call') === false) { $msg = $err['message'] ? \sprintf('Error waiting for execution loop: %s', $err['message']) : 'Error waiting for execution loop'; throw new \RuntimeException($msg); } } } while ($n < 1); $content = \stream_get_contents($down); \fclose($down); if ($content) { $shell->setScopeVariables(@\unserialize($content)); } throw new BreakException('Exiting main thread'); } // This is the child process. It's going to do all the work. if (!@\cli_set_process_title('psysh (loop)')) { // Fall back to `setproctitle` if that wasn't succesful. if (\function_exists('setproctitle')) { @\setproctitle('psysh (loop)'); } } // We won't be needing this one. \fclose($down); // Save this; we'll need to close it in `afterRun` $this->up = $up; } /** * Create a savegame at the start of each loop iteration. * * @param Shell $shell */ public function beforeLoop(Shell $shell) { $this->createSavegame(); } /** * Clean up old savegames at the end of each loop iteration. * * @param Shell $shell */ public function afterLoop(Shell $shell) { // if there's an old savegame hanging around, let's kill it. if (isset($this->savegame)) { \posix_kill($this->savegame, \SIGKILL); \pcntl_signal_dispatch(); } } /** * After the REPL session ends, send the scope variables back up to the main * thread (if this is a child thread). * * @param Shell $shell */ public function afterRun(Shell $shell) { // We're a child thread. Send the scope variables back up to the main thread. if (isset($this->up)) { \fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false))); \fclose($this->up); \posix_kill(\posix_getpid(), \SIGKILL); } } /** * Create a savegame fork. * * The savegame contains the current execution state, and can be resumed in * the event that the worker dies unexpectedly (for example, by encountering * a PHP fatal error). */ private function createSavegame() { // the current process will become the savegame $this->savegame = \posix_getpid(); $pid = \pcntl_fork(); if ($pid < 0) { throw new \RuntimeException('Unable to create savegame fork'); } elseif ($pid > 0) { // we're the savegame now... let's wait and see what happens \pcntl_waitpid($pid, $status); // worker exited cleanly, let's bail if (!\pcntl_wexitstatus($status)) { \posix_kill(\posix_getpid(), \SIGKILL); } // worker didn't exit cleanly, we'll need to have another go $this->createSavegame(); } } /** * Serialize all serializable return values. * * A naïve serialization will run into issues if there is a Closure or * SimpleXMLElement (among other things) in scope when exiting the execution * loop. We'll just ignore these unserializable classes, and serialize what * we can. * * @param array $return */ private function serializeReturn(array $return): string { $serializable = []; foreach ($return as $key => $value) { // No need to return magic variables if (Context::isSpecialVariableName($key)) { continue; } // Resources and Closures don't error, but they don't serialize well either. if (\is_resource($value) || $value instanceof \Closure) { continue; } if (\version_compare(\PHP_VERSION, '8.1', '>=') && $value instanceof \UnitEnum) { // Enums defined in the REPL session can't be unserialized. $ref = new \ReflectionObject($value); if (\strpos($ref->getFileName(), ": eval()'d code") !== false) { continue; } } try { @\serialize($value); $serializable[$key] = $value; } catch (\Throwable $e) { // we'll just ignore this one... } } return @\serialize($serializable); } } ExecutionLoop/Listener.php000064400000003452150250565140011646 0ustar00parser = $parserFactory->createParser(); } /** * Reload code on input. * * @param Shell $shell * @param string $input */ public function onInput(Shell $shell, string $input) { $this->reload($shell); } /** * Look through included files and update anything with a new timestamp. * * @param Shell $shell */ private function reload(Shell $shell) { \clearstatcache(); $modified = []; foreach (\get_included_files() as $file) { $timestamp = \filemtime($file); if (!isset($this->timestamps[$file])) { $this->timestamps[$file] = $timestamp; continue; } if ($this->timestamps[$file] === $timestamp) { continue; } if (!$this->lintFile($file)) { $msg = \sprintf('Modified file "%s" could not be reloaded', $file); $shell->writeException(new ParseErrorException($msg)); continue; } $modified[] = $file; $this->timestamps[$file] = $timestamp; } // switch (count($modified)) { // case 0: // return; // case 1: // printf("Reloading modified file: \"%s\"\n", str_replace(getcwd(), '.', $file)); // break; // default: // printf("Reloading %d modified files\n", count($modified)); // break; // } foreach ($modified as $file) { $flags = ( RUNKIT_IMPORT_FUNCTIONS | RUNKIT_IMPORT_CLASSES | RUNKIT_IMPORT_CLASS_METHODS | RUNKIT_IMPORT_CLASS_CONSTS | RUNKIT_IMPORT_CLASS_PROPS | RUNKIT_IMPORT_OVERRIDE ); // these two const cannot be used with RUNKIT_IMPORT_OVERRIDE in runkit7 if (\extension_loaded('runkit7')) { $flags &= ~RUNKIT_IMPORT_CLASS_PROPS & ~RUNKIT_IMPORT_CLASS_STATIC_PROPS; runkit7_import($file, $flags); } else { runkit_import($file, $flags); } } } /** * Should this file be re-imported? * * Use PHP-Parser to ensure that the file is valid PHP. * * @param string $file */ private function lintFile(string $file): bool { // first try to parse it try { $this->parser->parse(\file_get_contents($file)); } catch (\Throwable $e) { return false; } return true; } } Shell.php000064400000136316150250565140006341 0ustar00run(); * * @author Justin Hileman */ class Shell extends Application { const VERSION = 'v0.11.13'; /** @deprecated */ const PROMPT = '>>> '; /** @deprecated */ const BUFF_PROMPT = '... '; /** @deprecated */ const REPLAY = '--> '; /** @deprecated */ const RETVAL = '=> '; private $config; private $cleaner; private $output; private $originalVerbosity; private $readline; private $inputBuffer; private $code; private $codeBuffer; private $codeBufferOpen; private $codeStack; private $stdoutBuffer; private $context; private $includes; private $outputWantsNewline = false; private $loopListeners; private $autoCompleter; private $matchers = []; private $commandsMatcher; private $lastExecSuccess = true; private $nonInteractive = false; private $errorReporting; /** * Create a new Psy Shell. * * @param Configuration|null $config (default: null) */ public function __construct(Configuration $config = null) { $this->config = $config ?: new Configuration(); $this->cleaner = $this->config->getCodeCleaner(); $this->context = new Context(); $this->includes = []; $this->readline = $this->config->getReadline(); $this->inputBuffer = []; $this->codeStack = []; $this->stdoutBuffer = ''; $this->loopListeners = $this->getDefaultLoopListeners(); parent::__construct('Psy Shell', self::VERSION); $this->config->setShell($this); // Register the current shell session's config with \Psy\info \Psy\info($this->config); } /** * Check whether the first thing in a backtrace is an include call. * * This is used by the psysh bin to decide whether to start a shell on boot, * or to simply autoload the library. */ public static function isIncluded(array $trace): bool { $isIncluded = isset($trace[0]['function']) && \in_array($trace[0]['function'], ['require', 'include', 'require_once', 'include_once']); // Detect Composer PHP bin proxies. if ($isIncluded && \array_key_exists('_composer_autoload_path', $GLOBALS) && \preg_match('{[\\\\/]psysh$}', $trace[0]['file'])) { // If we're in a bin proxy, we'll *always* see one include, but we // care if we see a second immediately after that. return isset($trace[1]['function']) && \in_array($trace[1]['function'], ['require', 'include', 'require_once', 'include_once']); } return $isIncluded; } /** * Check if the currently running PsySH bin is a phar archive. */ public static function isPhar(): bool { return \class_exists("\Phar") && \Phar::running() !== '' && \strpos(__FILE__, \Phar::running(true)) === 0; } /** * Invoke a Psy Shell from the current context. * * @see Psy\debug * @deprecated will be removed in 1.0. Use \Psy\debug instead * * @param array $vars Scope variables from the calling context (default: []) * @param object|string $bindTo Bound object ($this) or class (self) value for the shell * * @return array Scope variables from the debugger session */ public static function debug(array $vars = [], $bindTo = null): array { return \Psy\debug($vars, $bindTo); } /** * Adds a command object. * * {@inheritdoc} * * @param BaseCommand $command A Symfony Console Command object * * @return BaseCommand The registered command */ public function add(BaseCommand $command): BaseCommand { if ($ret = parent::add($command)) { if ($ret instanceof ContextAware) { $ret->setContext($this->context); } if ($ret instanceof PresenterAware) { $ret->setPresenter($this->config->getPresenter()); } if (isset($this->commandsMatcher)) { $this->commandsMatcher->setCommands($this->all()); } } return $ret; } /** * Gets the default input definition. * * @return InputDefinition An InputDefinition instance */ protected function getDefaultInputDefinition(): InputDefinition { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), ]); } /** * Gets the default commands that should always be available. * * @return array An array of default Command instances */ protected function getDefaultCommands(): array { $sudo = new Command\SudoCommand(); $sudo->setReadline($this->readline); $hist = new Command\HistoryCommand(); $hist->setReadline($this->readline); return [ new Command\HelpCommand(), new Command\ListCommand(), new Command\DumpCommand(), new Command\DocCommand(), new Command\ShowCommand(), new Command\WtfCommand(), new Command\WhereamiCommand(), new Command\ThrowUpCommand(), new Command\TimeitCommand(), new Command\TraceCommand(), new Command\BufferCommand(), new Command\ClearCommand(), new Command\EditCommand($this->config->getRuntimeDir()), // new Command\PsyVersionCommand(), $sudo, $hist, new Command\ExitCommand(), ]; } /** * @return Matcher\AbstractMatcher[] */ protected function getDefaultMatchers(): array { // Store the Commands Matcher for later. If more commands are added, // we'll update the Commands Matcher too. $this->commandsMatcher = new Matcher\CommandsMatcher($this->all()); return [ $this->commandsMatcher, new Matcher\KeywordsMatcher(), new Matcher\VariablesMatcher(), new Matcher\ConstantsMatcher(), new Matcher\FunctionsMatcher(), new Matcher\ClassNamesMatcher(), new Matcher\ClassMethodsMatcher(), new Matcher\ClassAttributesMatcher(), new Matcher\ObjectMethodsMatcher(), new Matcher\ObjectAttributesMatcher(), new Matcher\ClassMethodDefaultParametersMatcher(), new Matcher\ObjectMethodDefaultParametersMatcher(), new Matcher\FunctionDefaultParametersMatcher(), ]; } /** * @deprecated Nothing should use this anymore */ protected function getTabCompletionMatchers() { @\trigger_error('getTabCompletionMatchers is no longer used', \E_USER_DEPRECATED); } /** * Gets the default command loop listeners. * * @return array An array of Execution Loop Listener instances */ protected function getDefaultLoopListeners(): array { $listeners = []; if (ProcessForker::isSupported() && $this->config->usePcntl()) { $listeners[] = new ProcessForker(); } if (RunkitReloader::isSupported()) { $listeners[] = new RunkitReloader(); } return $listeners; } /** * Add tab completion matchers. * * @param array $matchers */ public function addMatchers(array $matchers) { $this->matchers = \array_merge($this->matchers, $matchers); if (isset($this->autoCompleter)) { $this->addMatchersToAutoCompleter($matchers); } } /** * @deprecated Call `addMatchers` instead * * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->addMatchers($matchers); } /** * Set the Shell output. * * @param OutputInterface $output */ public function setOutput(OutputInterface $output) { $this->output = $output; $this->originalVerbosity = $output->getVerbosity(); } /** * Runs PsySH. * * @param InputInterface|null $input An Input instance * @param OutputInterface|null $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function run(InputInterface $input = null, OutputInterface $output = null): int { // We'll just ignore the input passed in, and set up our own! $input = new ArrayInput([]); if ($output === null) { $output = $this->config->getOutput(); } $this->setAutoExit(false); $this->setCatchExceptions(false); try { return parent::run($input, $output); } catch (\Throwable $e) { $this->writeException($e); } return 1; } /** * Runs PsySH. * * @throws \Throwable if thrown via the `throw-up` command * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output): int { $this->setOutput($output); $this->resetCodeBuffer(); if ($input->isInteractive()) { // @todo should it be possible to have raw output in an interactive run? return $this->doInteractiveRun(); } else { return $this->doNonInteractiveRun($this->config->rawOutput()); } } /** * Run PsySH in interactive mode. * * Initializes tab completion and readline history, then spins up the * execution loop. * * @throws \Throwable if thrown via the `throw-up` command * * @return int 0 if everything went fine, or an error code */ private function doInteractiveRun(): int { $this->initializeTabCompletion(); $this->readline->readHistory(); $this->output->writeln($this->getHeader()); $this->writeVersionInfo(); $this->writeStartupMessage(); try { $this->beforeRun(); $this->loadIncludes(); $loop = new ExecutionLoopClosure($this); $loop->execute(); $this->afterRun(); } catch (ThrowUpException $e) { throw $e->getPrevious(); } catch (BreakException $e) { // The ProcessForker throws a BreakException to finish the main thread. } return 0; } /** * Run PsySH in non-interactive mode. * * Note that this isn't very useful unless you supply "include" arguments at * the command line, or code via stdin. * * @param bool $rawOutput * * @return int 0 if everything went fine, or an error code */ private function doNonInteractiveRun(bool $rawOutput): int { $this->nonInteractive = true; // If raw output is enabled (or output is piped) we don't want startup messages. if (!$rawOutput && !$this->config->outputIsPiped()) { $this->output->writeln($this->getHeader()); $this->writeVersionInfo(); $this->writeStartupMessage(); } $this->beforeRun(); $this->loadIncludes(); // For non-interactive execution, read only from the input buffer or from piped input. // Otherwise it'll try to readline and hang, waiting for user input with no indication of // what's holding things up. if (!empty($this->inputBuffer) || $this->config->inputIsPiped()) { $this->getInput(false); } if ($this->hasCode()) { $ret = $this->execute($this->flushCode()); $this->writeReturnValue($ret, $rawOutput); } $this->afterRun(); $this->nonInteractive = false; return 0; } /** * Configures the input and output instances based on the user arguments and options. */ protected function configureIO(InputInterface $input, OutputInterface $output) { // @todo overrides via environment variables (or should these happen in config? ... probably config) $input->setInteractive($this->config->getInputInteractive()); if ($this->config->getOutputDecorated() !== null) { $output->setDecorated($this->config->getOutputDecorated()); } $output->setVerbosity($this->config->getOutputVerbosity()); } /** * Load user-defined includes. */ private function loadIncludes() { // Load user-defined includes $load = function (self $__psysh__) { \set_error_handler([$__psysh__, 'handleError']); foreach ($__psysh__->getIncludes() as $__psysh_include__) { try { include_once $__psysh_include__; } catch (\Exception $_e) { $__psysh__->writeException($_e); } } \restore_error_handler(); unset($__psysh_include__); // Override any new local variables with pre-defined scope variables \extract($__psysh__->getScopeVariables(false)); // ... then add the whole mess of variables back. $__psysh__->setScopeVariables(\get_defined_vars()); }; $load($this); } /** * Read user input. * * This will continue fetching user input until the code buffer contains * valid code. * * @throws BreakException if user hits Ctrl+D * * @param bool $interactive */ public function getInput(bool $interactive = true) { $this->codeBufferOpen = false; do { // reset output verbosity (in case it was altered by a subcommand) $this->output->setVerbosity($this->originalVerbosity); $input = $this->readline(); /* * Handle Ctrl+D. It behaves differently in different cases: * * 1) In an expression, like a function or "if" block, clear the input buffer * 2) At top-level session, behave like the exit command * 3) When non-interactive, return, because that's the end of stdin */ if ($input === false) { if (!$interactive) { return; } $this->output->writeln(''); if ($this->hasCode()) { $this->resetCodeBuffer(); } else { throw new BreakException('Ctrl+D'); } } // handle empty input if (\trim($input) === '' && !$this->codeBufferOpen) { continue; } $input = $this->onInput($input); // If the input isn't in an open string or comment, check for commands to run. if ($this->hasCommand($input) && !$this->inputInOpenStringOrComment($input)) { $this->addHistory($input); $this->runCommand($input); continue; } $this->addCode($input); } while (!$interactive || !$this->hasValidCode()); } /** * Check whether the code buffer (plus current input) is in an open string or comment. * * @param string $input current line of input * * @return bool true if the input is in an open string or comment */ private function inputInOpenStringOrComment(string $input): bool { if (!$this->hasCode()) { return false; } $code = $this->codeBuffer; $code[] = $input; $tokens = @\token_get_all('loopListeners as $listener) { $listener->beforeRun($this); } } /** * Run execution loop listeners at the start of each loop. */ public function beforeLoop() { foreach ($this->loopListeners as $listener) { $listener->beforeLoop($this); } } /** * Run execution loop listeners on user input. * * @param string $input */ public function onInput(string $input): string { foreach ($this->loopListeners as $listeners) { if (($return = $listeners->onInput($this, $input)) !== null) { $input = $return; } } return $input; } /** * Run execution loop listeners on code to be executed. * * @param string $code */ public function onExecute(string $code): string { $this->errorReporting = \error_reporting(); foreach ($this->loopListeners as $listener) { if (($return = $listener->onExecute($this, $code)) !== null) { $code = $return; } } $output = $this->output; if ($output instanceof ConsoleOutput) { $output = $output->getErrorOutput(); } $output->writeln(\sprintf('', OutputFormatter::escape($code)), ConsoleOutput::VERBOSITY_DEBUG); return $code; } /** * Run execution loop listeners after each loop. */ public function afterLoop() { foreach ($this->loopListeners as $listener) { $listener->afterLoop($this); } } /** * Run execution loop listers after the shell session. */ protected function afterRun() { foreach ($this->loopListeners as $listener) { $listener->afterRun($this); } } /** * Set the variables currently in scope. * * @param array $vars */ public function setScopeVariables(array $vars) { $this->context->setAll($vars); } /** * Return the set of variables currently in scope. * * @param bool $includeBoundObject Pass false to exclude 'this'. If you're * passing the scope variables to `extract` * in PHP 7.1+, you _must_ exclude 'this' * * @return array Associative array of scope variables */ public function getScopeVariables(bool $includeBoundObject = true): array { $vars = $this->context->getAll(); if (!$includeBoundObject) { unset($vars['this']); } return $vars; } /** * Return the set of magic variables currently in scope. * * @param bool $includeBoundObject Pass false to exclude 'this'. If you're * passing the scope variables to `extract` * in PHP 7.1+, you _must_ exclude 'this' * * @return array Associative array of magic scope variables */ public function getSpecialScopeVariables(bool $includeBoundObject = true): array { $vars = $this->context->getSpecialVariables(); if (!$includeBoundObject) { unset($vars['this']); } return $vars; } /** * Return the set of variables currently in scope which differ from the * values passed as $currentVars. * * This is used inside the Execution Loop Closure to pick up scope variable * changes made by commands while the loop is running. * * @param array $currentVars * * @return array Associative array of scope variables which differ from $currentVars */ public function getScopeVariablesDiff(array $currentVars): array { $newVars = []; foreach ($this->getScopeVariables(false) as $key => $value) { if (!\array_key_exists($key, $currentVars) || $currentVars[$key] !== $value) { $newVars[$key] = $value; } } return $newVars; } /** * Get the set of unused command-scope variable names. * * @return array Array of unused variable names */ public function getUnusedCommandScopeVariableNames(): array { return $this->context->getUnusedCommandScopeVariableNames(); } /** * Get the set of variable names currently in scope. * * @return array Array of variable names */ public function getScopeVariableNames(): array { return \array_keys($this->context->getAll()); } /** * Get a scope variable value by name. * * @param string $name * * @return mixed */ public function getScopeVariable(string $name) { return $this->context->get($name); } /** * Set the bound object ($this variable) for the interactive shell. * * @param object|null $boundObject */ public function setBoundObject($boundObject) { $this->context->setBoundObject($boundObject); } /** * Get the bound object ($this variable) for the interactive shell. * * @return object|null */ public function getBoundObject() { return $this->context->getBoundObject(); } /** * Set the bound class (self) for the interactive shell. * * @param string|null $boundClass */ public function setBoundClass($boundClass) { $this->context->setBoundClass($boundClass); } /** * Get the bound class (self) for the interactive shell. * * @return string|null */ public function getBoundClass() { return $this->context->getBoundClass(); } /** * Add includes, to be parsed and executed before running the interactive shell. * * @param array $includes */ public function setIncludes(array $includes = []) { $this->includes = $includes; } /** * Get PHP files to be parsed and executed before running the interactive shell. * * @return string[] */ public function getIncludes(): array { return \array_merge($this->config->getDefaultIncludes(), $this->includes); } /** * Check whether this shell's code buffer contains code. * * @return bool True if the code buffer contains code */ public function hasCode(): bool { return !empty($this->codeBuffer); } /** * Check whether the code in this shell's code buffer is valid. * * If the code is valid, the code buffer should be flushed and evaluated. * * @return bool True if the code buffer content is valid */ protected function hasValidCode(): bool { return !$this->codeBufferOpen && $this->code !== false; } /** * Add code to the code buffer. * * @param string $code * @param bool $silent */ public function addCode(string $code, bool $silent = false) { try { // Code lines ending in \ keep the buffer open if (\substr(\rtrim($code), -1) === '\\') { $this->codeBufferOpen = true; $code = \substr(\rtrim($code), 0, -1); } else { $this->codeBufferOpen = false; } $this->codeBuffer[] = $silent ? new SilentInput($code) : $code; $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons()); } catch (\Throwable $e) { // Add failed code blocks to the readline history. $this->addCodeBufferToHistory(); throw $e; } } /** * Set the code buffer. * * This is mostly used by `Shell::execute`. Any existing code in the input * buffer is pushed onto a stack and will come back after this new code is * executed. * * @throws \InvalidArgumentException if $code isn't a complete statement * * @param string $code * @param bool $silent */ private function setCode(string $code, bool $silent = false) { if ($this->hasCode()) { $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code]; } $this->resetCodeBuffer(); try { $this->addCode($code, $silent); } catch (\Throwable $e) { $this->popCodeStack(); throw $e; } if (!$this->hasValidCode()) { $this->popCodeStack(); throw new \InvalidArgumentException('Unexpected end of input'); } } /** * Get the current code buffer. * * This is useful for commands which manipulate the buffer. * * @return string[] */ public function getCodeBuffer(): array { return $this->codeBuffer; } /** * Run a Psy Shell command given the user input. * * @throws \InvalidArgumentException if the input is not a valid command * * @param string $input User input string * * @return mixed Who knows? */ protected function runCommand(string $input) { $command = $this->getCommand($input); if (empty($command)) { throw new \InvalidArgumentException('Command not found: '.$input); } $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;"))); if ($input->hasParameterOption(['--help', '-h'])) { $helpCommand = $this->get('help'); if (!$helpCommand instanceof Command\HelpCommand) { throw new RuntimeException('Invalid help command instance'); } $helpCommand->setCommand($command); return $helpCommand->run(new StringInput(''), $this->output); } return $command->run($input, $this->output); } /** * Reset the current code buffer. * * This should be run after evaluating user input, catching exceptions, or * on demand by commands such as BufferCommand. */ public function resetCodeBuffer() { $this->codeBuffer = []; $this->code = false; } /** * Inject input into the input buffer. * * This is useful for commands which want to replay history. * * @param string|array $input * @param bool $silent */ public function addInput($input, bool $silent = false) { foreach ((array) $input as $line) { $this->inputBuffer[] = $silent ? new SilentInput($line) : $line; } } /** * Flush the current (valid) code buffer. * * If the code buffer is valid, resets the code buffer and returns the * current code. * * @return string|null PHP code buffer contents */ public function flushCode() { if ($this->hasValidCode()) { $this->addCodeBufferToHistory(); $code = $this->code; $this->popCodeStack(); return $code; } } /** * Reset the code buffer and restore any code pushed during `execute` calls. */ private function popCodeStack() { $this->resetCodeBuffer(); if (empty($this->codeStack)) { return; } list($codeBuffer, $codeBufferOpen, $code) = \array_pop($this->codeStack); $this->codeBuffer = $codeBuffer; $this->codeBufferOpen = $codeBufferOpen; $this->code = $code; } /** * (Possibly) add a line to the readline history. * * Like Bash, if the line starts with a space character, it will be omitted * from history. Note that an entire block multi-line code input will be * omitted iff the first line begins with a space. * * Additionally, if a line is "silent", i.e. it was initially added with the * silent flag, it will also be omitted. * * @param string|SilentInput $line */ private function addHistory($line) { if ($line instanceof SilentInput) { return; } // Skip empty lines and lines starting with a space if (\trim($line) !== '' && \substr($line, 0, 1) !== ' ') { $this->readline->addHistory($line); } } /** * Filter silent input from code buffer, write the rest to readline history. */ private function addCodeBufferToHistory() { $codeBuffer = \array_filter($this->codeBuffer, function ($line) { return !$line instanceof SilentInput; }); $this->addHistory(\implode("\n", $codeBuffer)); } /** * Get the current evaluation scope namespace. * * @see CodeCleaner::getNamespace * * @return string|null Current code namespace */ public function getNamespace() { if ($namespace = $this->cleaner->getNamespace()) { return \implode('\\', $namespace); } } /** * Write a string to stdout. * * This is used by the shell loop for rendering output from evaluated code. * * @param string $out * @param int $phase Output buffering phase */ public function writeStdout(string $out, int $phase = \PHP_OUTPUT_HANDLER_END) { if ($phase & \PHP_OUTPUT_HANDLER_START) { if ($this->output instanceof ShellOutput) { $this->output->startPaging(); } } $isCleaning = $phase & \PHP_OUTPUT_HANDLER_CLEAN; // Incremental flush if ($out !== '' && !$isCleaning) { $this->output->write($out, false, OutputInterface::OUTPUT_RAW); $this->outputWantsNewline = (\substr($out, -1) !== "\n"); $this->stdoutBuffer .= $out; } // Output buffering is done! if ($phase & \PHP_OUTPUT_HANDLER_END) { // Write an extra newline if stdout didn't end with one if ($this->outputWantsNewline) { if (!$this->config->rawOutput() && !$this->config->outputIsPiped()) { $this->output->writeln(\sprintf('%s', $this->config->useUnicode() ? '⏎' : '\\n')); } else { $this->output->writeln(''); } $this->outputWantsNewline = false; } // Save the stdout buffer as $__out if ($this->stdoutBuffer !== '') { $this->context->setLastStdout($this->stdoutBuffer); $this->stdoutBuffer = ''; } if ($this->output instanceof ShellOutput) { $this->output->stopPaging(); } } } /** * Write a return value to stdout. * * The return value is formatted or pretty-printed, and rendered in a * visibly distinct manner (in this case, as cyan). * * @see self::presentValue * * @param mixed $ret * @param bool $rawOutput Write raw var_export-style values */ public function writeReturnValue($ret, bool $rawOutput = false) { $this->lastExecSuccess = true; if ($ret instanceof NoReturnValue) { return; } $this->context->setReturnValue($ret); if ($rawOutput) { $formatted = \var_export($ret, true); } else { $prompt = $this->config->theme()->returnValue(); $indent = \str_repeat(' ', \strlen($prompt)); $formatted = $this->presentValue($ret); $formattedRetValue = \sprintf('%s', $prompt); $formatted = $formattedRetValue.\str_replace(\PHP_EOL, \PHP_EOL.$indent, $formatted); } if ($this->output instanceof ShellOutput) { $this->output->page($formatted.\PHP_EOL); } else { $this->output->writeln($formatted); } } /** * Renders a caught Exception or Error. * * Exceptions are formatted according to severity. ErrorExceptions which were * warnings or Strict errors aren't rendered as harshly as real errors. * * Stores $e as the last Exception in the Shell Context. * * @param \Throwable $e An exception or error instance */ public function writeException(\Throwable $e) { // No need to write the break exception during a non-interactive run. if ($e instanceof BreakException && $this->nonInteractive) { $this->resetCodeBuffer(); return; } // Break exceptions don't count :) if (!$e instanceof BreakException) { $this->lastExecSuccess = false; $this->context->setLastException($e); } $output = $this->output; if ($output instanceof ConsoleOutput) { $output = $output->getErrorOutput(); } if (!$this->config->theme()->compact()) { $output->writeln(''); } $output->writeln($this->formatException($e)); if (!$this->config->theme()->compact()) { $output->writeln(''); } // Include an exception trace (as long as this isn't a BreakException). if (!$e instanceof BreakException && $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { $trace = TraceFormatter::formatTrace($e); if (\count($trace) !== 0) { $output->writeln('--'); $output->write($trace, true); $output->writeln(''); } } $this->resetCodeBuffer(); } /** * Check whether the last exec was successful. * * Returns true if a return value was logged rather than an exception. */ public function getLastExecSuccess(): bool { return $this->lastExecSuccess; } /** * Helper for formatting an exception or error for writeException(). * * @todo extract this to somewhere it makes more sense * * @param \Throwable $e */ public function formatException(\Throwable $e): string { $indent = $this->config->theme()->compact() ? '' : ' '; if ($e instanceof BreakException) { return \sprintf('%s INFO %s.', $indent, \rtrim($e->getRawMessage(), '.')); } elseif ($e instanceof PsyException) { $message = $e->getLine() > 1 ? \sprintf('%s in %s on line %d', $e->getRawMessage(), $e->getFile(), $e->getLine()) : \sprintf('%s in %s', $e->getRawMessage(), $e->getFile()); $messageLabel = \strtoupper($this->getMessageLabel($e)); } else { $message = $e->getMessage(); $messageLabel = $this->getMessageLabel($e); } $message = \preg_replace( "#(\\w:)?([\\\\/]\\w+)*[\\\\/]src[\\\\/]Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#", "eval()'d code", $message ); $message = \str_replace(" in eval()'d code", '', $message); $message = \trim($message); // Ensures the given string ends with punctuation... if (!empty($message) && !\in_array(\substr($message, -1), ['.', '?', '!', ':'])) { $message = "$message."; } // Ensures the given message only contains relative paths... $message = \str_replace(\getcwd().\DIRECTORY_SEPARATOR, '', $message); $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error'; return \sprintf('%s<%s> %s %s', $indent, $severity, $messageLabel, $severity, OutputFormatter::escape($message)); } /** * Helper for getting an output style for the given ErrorException's level. * * @param \ErrorException $e */ protected function getSeverity(\ErrorException $e): string { $severity = $e->getSeverity(); if ($severity & \error_reporting()) { switch ($severity) { case \E_WARNING: case \E_NOTICE: case \E_CORE_WARNING: case \E_COMPILE_WARNING: case \E_USER_WARNING: case \E_USER_NOTICE: case \E_USER_DEPRECATED: case \E_DEPRECATED: case \E_STRICT: return 'warning'; default: return 'error'; } } else { // Since this is below the user's reporting threshold, it's always going to be a warning. return 'warning'; } } /** * Helper for getting an output style for the given ErrorException's level. * * @param \Throwable $e */ protected function getMessageLabel(\Throwable $e): string { if ($e instanceof \ErrorException) { $severity = $e->getSeverity(); if ($severity & \error_reporting()) { switch ($severity) { case \E_WARNING: return 'Warning'; case \E_NOTICE: return 'Notice'; case \E_CORE_WARNING: return 'Core Warning'; case \E_COMPILE_WARNING: return 'Compile Warning'; case \E_USER_WARNING: return 'User Warning'; case \E_USER_NOTICE: return 'User Notice'; case \E_USER_DEPRECATED: return 'User Deprecated'; case \E_DEPRECATED: return 'Deprecated'; case \E_STRICT: return 'Strict'; } } } if ($e instanceof PsyException) { $exceptionShortName = (new \ReflectionClass($e))->getShortName(); $typeParts = \preg_split('/(?=[A-Z])/', $exceptionShortName); \array_pop($typeParts); // Removes "Exception" return \trim(\strtoupper(\implode(' ', $typeParts))); } return \get_class($e); } /** * Execute code in the shell execution context. * * @param string $code * @param bool $throwExceptions * * @return mixed */ public function execute(string $code, bool $throwExceptions = false) { $this->setCode($code, true); $closure = new ExecutionClosure($this); if ($throwExceptions) { return $closure->execute(); } try { return $closure->execute(); } catch (\Throwable $_e) { $this->writeException($_e); } } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler([$psysh, 'handleError']); * * Unlike ErrorException::throwException, this error handler respects error * levels; i.e. it logs warnings and notices, but doesn't throw exceptions. * This should probably only be used in the inner execution loop of the * shell, as most of the time a thrown exception is much more useful. * * If the error type matches the `errorLoggingLevel` config, it will be * logged as well, regardless of the `error_reporting` level. * * @see \Psy\Exception\ErrorException::throwException * @see \Psy\Shell::writeException * * @throws \Psy\Exception\ErrorException depending on the error level * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public function handleError($errno, $errstr, $errfile, $errline) { // This is an error worth throwing. // // n.b. Technically we can't handle all of these in userland code, but // we'll list 'em all for good measure if ($errno & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR)) { ErrorException::throwException($errno, $errstr, $errfile, $errline); } // When errors are suppressed, the error_reporting value will differ // from when we started executing. In that case, we won't log errors. $errorsSuppressed = $this->errorReporting !== null && $this->errorReporting !== \error_reporting(); // Otherwise log it and continue. if ($errno & \error_reporting() || (!$errorsSuppressed && ($errno & $this->config->errorLoggingLevel()))) { $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline)); } } /** * Format a value for display. * * @see Presenter::present * * @param mixed $val * * @return string Formatted value */ protected function presentValue($val): string { return $this->config->getPresenter()->present($val); } /** * Get a command (if one exists) for the current input string. * * @param string $input * * @return BaseCommand|null */ protected function getCommand(string $input) { $input = new StringInput($input); if ($name = $input->getFirstArgument()) { return $this->get($name); } } /** * Check whether a command is set for the current input string. * * @param string $input * * @return bool True if the shell has a command for the given input */ protected function hasCommand(string $input): bool { if (\preg_match('/([^\s]+?)(?:\s|$)/A', \ltrim($input), $match)) { return $this->has($match[1]); } return false; } /** * Get the current input prompt. * * @return string|null */ protected function getPrompt() { if ($this->output->isQuiet()) { return null; } $theme = $this->config->theme(); if ($this->hasCode()) { return $theme->bufferPrompt(); } return $theme->prompt(); } /** * Read a line of user input. * * This will return a line from the input buffer (if any exist). Otherwise, * it will ask the user for input. * * If readline is enabled, this delegates to readline. Otherwise, it's an * ugly `fgets` call. * * @param bool $interactive * * @return string|false One line of user input */ protected function readline(bool $interactive = true) { $prompt = $this->config->theme()->replayPrompt(); if (!empty($this->inputBuffer)) { $line = \array_shift($this->inputBuffer); if (!$line instanceof SilentInput) { $this->output->writeln(\sprintf('%s', $prompt, OutputFormatter::escape($line))); } return $line; } $bracketedPaste = $interactive && $this->config->useBracketedPaste(); if ($bracketedPaste) { \printf("\e[?2004h"); // Enable bracketed paste } $line = $this->readline->readline($this->getPrompt()); if ($bracketedPaste) { \printf("\e[?2004l"); // ... and disable it again } return $line; } /** * Get the shell output header. */ protected function getHeader(): string { return \sprintf('%s by Justin Hileman', $this->getVersion()); } /** * Get the current version of Psy Shell. * * @deprecated call self::getVersionHeader instead */ public function getVersion(): string { return self::getVersionHeader($this->config->useUnicode()); } /** * Get a pretty header including the current version of Psy Shell. * * @param bool $useUnicode */ public static function getVersionHeader(bool $useUnicode = false): string { $separator = $useUnicode ? '—' : '-'; return \sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, \PHP_VERSION, $separator, \PHP_SAPI); } /** * Get a PHP manual database instance. * * @return \PDO|null */ public function getManualDb() { return $this->config->getManualDb(); } /** * @deprecated Tab completion is provided by the AutoCompleter service */ protected function autocomplete($text) { @\trigger_error('Tab completion is provided by the AutoCompleter service', \E_USER_DEPRECATED); } /** * Initialize tab completion matchers. * * If tab completion is enabled this adds tab completion matchers to the * auto completer and sets context if needed. */ protected function initializeTabCompletion() { if (!$this->config->useTabCompletion()) { return; } $this->autoCompleter = $this->config->getAutoCompleter(); // auto completer needs shell to be linked to configuration because of // the context aware matchers $this->addMatchersToAutoCompleter($this->getDefaultMatchers()); $this->addMatchersToAutoCompleter($this->matchers); $this->autoCompleter->activate(); } /** * Add matchers to the auto completer, setting context if needed. * * @param array $matchers */ private function addMatchersToAutoCompleter(array $matchers) { foreach ($matchers as $matcher) { if ($matcher instanceof ContextAware) { $matcher->setContext($this->context); } $this->autoCompleter->addMatcher($matcher); } } /** * @todo Implement prompt to start update * * @return void|string */ protected function writeVersionInfo() { if (\PHP_SAPI !== 'cli') { return; } try { $client = $this->config->getChecker(); if (!$client->isLatest()) { $this->output->writeln(\sprintf('New version is available at psysh.org/psysh (current: %s, latest: %s)', self::VERSION, $client->getLatest())); } } catch (\InvalidArgumentException $e) { $this->output->writeln($e->getMessage()); } } /** * Write a startup message if set. */ protected function writeStartupMessage() { $message = $this->config->getStartupMessage(); if ($message !== null && $message !== '') { $this->output->writeln($message); } } } TabCompletion/AutoCompleter.php000064400000005526150250565140012613 0ustar00 */ class AutoCompleter { /** @var Matcher\AbstractMatcher[] */ protected $matchers; /** * Register a tab completion Matcher. * * @param AbstractMatcher $matcher */ public function addMatcher(AbstractMatcher $matcher) { $this->matchers[] = $matcher; } /** * Activate readline tab completion. */ public function activate() { \readline_completion_function([&$this, 'callback']); } /** * Handle readline completion. * * @param string $input Readline current word * @param int $index Current word index * @param array $info readline_info() data * * @return array */ public function processCallback(string $input, int $index, array $info = []): array { // Some (Windows?) systems provide incomplete `readline_info`, so let's // try to work around it. $line = $info['line_buffer']; if (isset($info['end'])) { $line = \substr($line, 0, $info['end']); } if ($line === '' && $input !== '') { $line = $input; } $tokens = \token_get_all('matchers as $matcher) { if ($matcher->hasMatched($tokens)) { $matches = \array_merge($matcher->getMatches($tokens), $matches); } } $matches = \array_unique($matches); return !empty($matches) ? $matches : ['']; } /** * The readline_completion_function callback handler. * * @see processCallback * * @param string $input * @param int $index * * @return array */ public function callback(string $input, int $index): array { return $this->processCallback($input, $index, \readline_info()); } /** * Remove readline callback handler on destruct. */ public function __destruct() { // PHP didn't implement the whole readline API when they first switched // to libedit. And they still haven't. if (\function_exists('readline_callback_handler_remove')) { \readline_callback_handler_remove(); } } } TabCompletion/Matcher/AbstractDefaultParametersMatcher.php000064400000004026150250565140020005 0ustar00isDefaultValueAvailable()) { return []; } $defaultValue = $this->valueToShortString($parameter->getDefaultValue()); $parametersProcessed[] = \sprintf('$%s = %s', $parameter->getName(), $defaultValue); } if (empty($parametersProcessed)) { return []; } return [\implode(', ', $parametersProcessed).')']; } /** * Takes in the default value of a parameter and turns it into a * string representation that fits inline. * This is not 100% true to the original (newlines are inlined, for example). * * @param mixed $value */ private function valueToShortString($value): string { if (!\is_array($value)) { return \json_encode($value); } $chunks = []; $chunksSequential = []; $allSequential = true; foreach ($value as $key => $item) { $allSequential = $allSequential && \is_numeric($key) && $key === \count($chunksSequential); $keyString = $this->valueToShortString($key); $itemString = $this->valueToShortString($item); $chunks[] = "{$keyString} => {$itemString}"; $chunksSequential[] = $itemString; } $chunksToImplode = $allSequential ? $chunksSequential : $chunks; return '['.\implode(', ', $chunksToImplode).']'; } } TabCompletion/Matcher/ObjectAttributesMatcher.php000064400000003622150250565140016167 0ustar00 */ class ObjectAttributesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; } $objectName = \str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return []; } if (!\is_object($object)) { return []; } return \array_filter( \array_keys(\get_class_vars(\get_class($object))), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } TabCompletion/Matcher/MongoDatabaseMatcher.php000064400000003224150250565140015414 0ustar00 */ class MongoDatabaseMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); $objectName = \str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoDB) { return []; } return \array_filter( $object->getCollectionNames(), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } TabCompletion/Matcher/CommandsMatcher.php000064400000004706150250565140014457 0ustar00 */ class CommandsMatcher extends AbstractMatcher { /** @var string[] */ protected $commands = []; /** * CommandsMatcher constructor. * * @param Command[] $commands */ public function __construct(array $commands) { $this->setCommands($commands); } /** * Set Commands for completion. * * @param Command[] $commands */ public function setCommands(array $commands) { $names = []; foreach ($commands as $command) { $names = \array_merge([$command->getName()], $names); $names = \array_merge($command->getAliases(), $names); } $this->commands = $names; } /** * Check whether a command $name is defined. * * @param string $name */ protected function isCommand(string $name): bool { return \in_array($name, $this->commands); } /** * Check whether input matches a defined command. * * @param string $name */ protected function matchCommand(string $name): bool { foreach ($this->commands as $cmd) { if ($this->startsWith($name, $cmd)) { return true; } } return false; } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); return \array_filter($this->commands, function ($command) use ($input) { return AbstractMatcher::startsWith($input, $command); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { /* $openTag */ \array_shift($tokens); $command = \array_shift($tokens); switch (true) { case self::tokenIs($command, self::T_STRING) && !$this->isCommand($command[1]) && $this->matchCommand($command[1]) && empty($tokens): return true; } return false; } } TabCompletion/Matcher/ObjectMethodsMatcher.php000064400000004040150250565140015437 0ustar00 */ class ObjectMethodsMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; } $objectName = \str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return []; } if (!\is_object($object)) { return []; } return \array_filter( \get_class_methods($object), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var) && // also check that we do not suggest invoking a super method(__construct, __wakeup, …) !AbstractMatcher::startsWith('__', $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } TabCompletion/Matcher/ClassAttributesMatcher.php000064400000004321150250565140016023 0ustar00 */ class ClassAttributesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator \array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $re) { return []; } $vars = \array_merge( \array_map( function ($var) { return '$'.$var; }, \array_keys($reflection->getStaticProperties()) ), \array_keys($reflection->getConstants()) ); return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); $className = \array_pop($chunks); return $className.'::'.$name; }, \array_filter( $vars, function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php000064400000003155150250565140020452 0ustar00getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $e) { // In this case the class apparently does not exist, so we can do nothing return []; } $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); foreach ($methods as $method) { if ($method->getName() === $functionName[1]) { return $this->getDefaultParameterCompletion($method->getParameters()); } } return []; } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } $operator = \array_pop($tokens); if (!self::tokenIs($operator, self::T_DOUBLE_COLON)) { return false; } return true; } } TabCompletion/Matcher/ConstantsMatcher.php000064400000002515150250565140014666 0ustar00 */ class ConstantsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $const = $this->getInput($tokens); return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($const) { return AbstractMatcher::startsWith($const, $constant); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): case self::isOperator($token): return true; } return false; } } TabCompletion/Matcher/FunctionDefaultParametersMatcher.php000064400000002314150250565140020025 0ustar00getParameters(); return $this->getDefaultParameterCompletion($parameters); } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } if (!\function_exists($functionName[1])) { return false; } return true; } } TabCompletion/Matcher/ClassMethodsMatcher.php000064400000004277150250565140015312 0ustar00 */ class ClassMethodsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator \array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $re) { return []; } if (self::needCompleteClass($tokens[1])) { $methods = $reflection->getMethods(); } else { $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); } $methods = \array_map(function (\ReflectionMethod $method) { return $method->getName(); }, $methods); return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); $className = \array_pop($chunks); return $className.'::'.$name; }, \array_filter($methods, function ($method) use ($input) { return AbstractMatcher::startsWith($input, $method); }) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } TabCompletion/Matcher/FunctionsMatcher.php000064400000002604150250565140014661 0ustar00 */ class FunctionsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $func = $this->getInput($tokens); $functions = \get_defined_functions(); $allFunctions = \array_merge($functions['user'], $functions['internal']); return \array_filter($allFunctions, function ($function) use ($func) { return AbstractMatcher::startsWith($func, $function); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): return false; case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): case self::isOperator($token): return true; } return false; } } TabCompletion/Matcher/MongoClientMatcher.php000064400000003417150250565140015132 0ustar00 */ class MongoClientMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); $objectName = \str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoClient) { return []; } $list = $object->listDBs(); return \array_filter( \array_map(function ($info) { return $info['name']; }, $list['databases']), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } TabCompletion/Matcher/AbstractContextAwareMatcher.php000064400000002472150250565140017004 0ustar00 */ abstract class AbstractContextAwareMatcher extends AbstractMatcher implements ContextAware { /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get a Context variable by name. * * @param string $var Variable name * * @return mixed */ protected function getVariable(string $var) { return $this->context->get($var); } /** * Get all variables in the current Context. * * @return array */ protected function getVariables(): array { return $this->context->getAll(); } } TabCompletion/Matcher/VariablesMatcher.php000064400000002355150250565140014624 0ustar00 */ class VariablesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $var = \str_replace('$', '', $this->getInput($tokens)); return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($var) { return AbstractMatcher::startsWith($var, $variable); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); switch (true) { case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): case \is_string($token) && $token === '$': case self::isOperator($token): return true; } return false; } } TabCompletion/Matcher/KeywordsMatcher.php000064400000004054150250565140014521 0ustar00 */ class KeywordsMatcher extends AbstractMatcher { protected $keywords = [ 'array', 'clone', 'declare', 'die', 'echo', 'empty', 'eval', 'exit', 'include', 'include_once', 'isset', 'list', 'print', 'require', 'require_once', 'unset', ]; protected $mandatoryStartKeywords = [ 'die', 'echo', 'print', 'unset', ]; /** * Get all (completable) PHP keywords. * * @return string[] */ public function getKeywords(): array { return $this->keywords; } /** * Check whether $keyword is a (completable) PHP keyword. * * @param string $keyword */ public function isKeyword(string $keyword): bool { return \in_array($keyword, $this->keywords); } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); return \array_filter($this->keywords, function ($keyword) use ($input) { return AbstractMatcher::startsWith($input, $keyword); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): // case is_string($token) && $token === '$': case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $prevToken) && self::tokenIs($token, self::T_STRING): case self::isOperator($token): return true; } return false; } } TabCompletion/Matcher/ClassNamesMatcher.php000064400000004600150250565140014740 0ustar00 */ class ClassNamesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $class = $this->getNamespaceAndClass($tokens); if ($class !== '' && $class[0] === '\\') { $class = \substr($class, 1, \strlen($class)); } $quotedClass = \preg_quote($class); return \array_map( function ($className) use ($class) { // get the number of namespace separators $nsPos = \substr_count($class, '\\'); $pieces = \explode('\\', $className); // $methods = Mirror::get($class); return \implode('\\', \array_slice($pieces, $nsPos, \count($pieces))); }, \array_filter( \array_merge(\get_declared_classes(), \get_declared_interfaces()), function ($className) use ($quotedClass) { return AbstractMatcher::startsWith($quotedClass, $className); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); $ignoredTokens = [ self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, ]; switch (true) { case self::hasToken([$ignoredTokens], $token): case self::hasToken([$ignoredTokens], $prevToken): case \is_string($token) && $token === '$': return false; case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR, self::T_STRING], $prevToken): case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): case self::isOperator($token): return true; } return false; } } TabCompletion/Matcher/AbstractMatcher.php000064400000011506150250565140014455 0ustar00 */ abstract class AbstractMatcher { /** Syntax types */ const CONSTANT_SYNTAX = '^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const VAR_SYNTAX = '^\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const MISC_OPERATORS = '+-*/^|&'; /** Token values */ const T_OPEN_TAG = 'T_OPEN_TAG'; const T_VARIABLE = 'T_VARIABLE'; const T_OBJECT_OPERATOR = 'T_OBJECT_OPERATOR'; const T_DOUBLE_COLON = 'T_DOUBLE_COLON'; const T_NEW = 'T_NEW'; const T_CLONE = 'T_CLONE'; const T_NS_SEPARATOR = 'T_NS_SEPARATOR'; const T_STRING = 'T_STRING'; const T_NAME_QUALIFIED = 'T_NAME_QUALIFIED'; const T_WHITESPACE = 'T_WHITESPACE'; const T_AND_EQUAL = 'T_AND_EQUAL'; const T_BOOLEAN_AND = 'T_BOOLEAN_AND'; const T_BOOLEAN_OR = 'T_BOOLEAN_OR'; const T_ENCAPSED_AND_WHITESPACE = 'T_ENCAPSED_AND_WHITESPACE'; const T_REQUIRE = 'T_REQUIRE'; const T_REQUIRE_ONCE = 'T_REQUIRE_ONCE'; const T_INCLUDE = 'T_INCLUDE'; const T_INCLUDE_ONCE = 'T_INCLUDE_ONCE'; /** * Check whether this matcher can provide completions for $tokens. * * @param array $tokens Tokenized readline input * * @return false */ public function hasMatched(array $tokens): bool { return false; } /** * Get current readline input word. * * @param array $tokens Tokenized readline input (see token_get_all) */ protected function getInput(array $tokens): string { $var = ''; $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { $var = $firstToken[1]; } return $var; } /** * Get current namespace and class (if any) from readline input. * * @param array $tokens Tokenized readline input (see token_get_all) */ protected function getNamespaceAndClass(array $tokens): string { $class = ''; while (self::hasToken( [self::T_NS_SEPARATOR, self::T_STRING, self::T_NAME_QUALIFIED], $token = \array_pop($tokens) )) { if (self::needCompleteClass($token)) { continue; } $class = $token[1].$class; } return $class; } /** * Provide tab completion matches for readline input. * * @param array $tokens information substracted with get_token_all * @param array $info readline_info object * * @return array The matches resulting from the query */ abstract public function getMatches(array $tokens, array $info = []): array; /** * Check whether $word starts with $prefix. * * @param string $prefix * @param string $word */ public static function startsWith(string $prefix, string $word): bool { return \preg_match(\sprintf('#^%s#', $prefix), $word); } /** * Check whether $token matches a given syntax pattern. * * @param mixed $token A PHP token (see token_get_all) * @param string $syntax A syntax pattern (default: variable pattern) */ public static function hasSyntax($token, string $syntax = self::VAR_SYNTAX): bool { if (!\is_array($token)) { return false; } $regexp = \sprintf('#%s#', $syntax); return (bool) \preg_match($regexp, $token[1]); } /** * Check whether $token type is $which. * * @param mixed $token A PHP token (see token_get_all) * @param string $which A PHP token type */ public static function tokenIs($token, string $which): bool { if (!\is_array($token)) { return false; } return \token_name($token[0]) === $which; } /** * Check whether $token is an operator. * * @param mixed $token A PHP token (see token_get_all) */ public static function isOperator($token): bool { if (!\is_string($token)) { return false; } return \strpos(self::MISC_OPERATORS, $token) !== false; } public static function needCompleteClass($token): bool { return \in_array($token[1], ['doc', 'ls', 'show']); } /** * Check whether $token type is present in $coll. * * @param array $coll A list of token types * @param mixed $token A PHP token (see token_get_all) */ public static function hasToken(array $coll, $token): bool { if (!\is_array($token)) { return false; } return \in_array(\token_name($token[0]), $coll); } } TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php000064400000003375150250565140020617 0ustar00getVariable($objectName); $reflection = new \ReflectionObject($object); } catch (\InvalidArgumentException $e) { return []; } catch (\ReflectionException $e) { return []; } $methods = $reflection->getMethods(); foreach ($methods as $method) { if ($method->getName() === $functionName[1]) { return $this->getDefaultParameterCompletion($method->getParameters()); } } return []; } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } $operator = \array_pop($tokens); if (!self::tokenIs($operator, self::T_OBJECT_OPERATOR)) { return false; } return true; } } ExecutionLoopClosure.php000064400000005475150250565140011425 0ustar00setClosure($__psysh__, function () use ($__psysh__) { // Restore execution scope variables \extract($__psysh__->getScopeVariables(false)); while (true) { $__psysh__->beforeLoop(); try { $__psysh__->getInput(); try { // Pull in any new execution scope variables if ($__psysh__->getLastExecSuccess()) { \extract($__psysh__->getScopeVariablesDiff(\get_defined_vars())); } // Buffer stdout; we'll need it later \ob_start([$__psysh__, 'writeStdout'], 1); // Convert all errors to exceptions \set_error_handler([$__psysh__, 'handleError']); // Evaluate the current code buffer $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: ExecutionClosure::NOOP_INPUT)); } catch (\Throwable $_e) { // Clean up on our way out. if (\ob_get_level() > 0) { \ob_end_clean(); } throw $_e; } finally { // Won't be needing this anymore \restore_error_handler(); } // Flush stdout (write to shell output, plus save to magic variable) \ob_end_flush(); // Save execution scope variables for next time $__psysh__->setScopeVariables(\get_defined_vars()); $__psysh__->writeReturnValue($_); } catch (BreakException $_e) { $__psysh__->writeException($_e); return; } catch (ThrowUpException $_e) { $__psysh__->writeException($_e); throw $_e; } catch (\Throwable $_e) { $__psysh__->writeException($_e); } $__psysh__->afterLoop(); } }); } } Input/SilentInput.php000064400000001567150250565140010646 0ustar00inputString = $inputString; } /** * To. String. */ public function __toString(): string { return $this->inputString; } } Input/ShellInput.php000064400000025622150250565140010455 0ustar00tokenPairs = $this->tokenize($input); } /** * {@inheritdoc} * * @throws \InvalidArgumentException if $definition has CodeArgument before the final argument position */ public function bind(InputDefinition $definition) { $hasCodeArgument = false; if ($definition->getArgumentCount() > 0) { $args = $definition->getArguments(); $lastArg = \array_pop($args); foreach ($args as $arg) { if ($arg instanceof CodeArgument) { $msg = \sprintf('Unexpected CodeArgument before the final position: %s', $arg->getName()); throw new \InvalidArgumentException($msg); } } if ($lastArg instanceof CodeArgument) { $hasCodeArgument = true; } } $this->hasCodeArgument = $hasCodeArgument; return parent::bind($definition); } /** * Tokenizes a string. * * The version of this on StringInput is good, but doesn't handle code * arguments if they're at all complicated. This does :) * * @param string $input The input to tokenize * * @return array An array of token/rest pairs * * @throws \InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize(string $input): array { $tokens = []; $length = \strlen($input); $cursor = 0; while ($cursor < $length) { if (\preg_match('/\s+/A', $input, $match, 0, $cursor)) { } elseif (\preg_match('/([^="\'\s]+?)(=?)('.StringInput::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) { $tokens[] = [ $match[1].$match[2].\stripcslashes(\str_replace(['"\'', '\'"', '\'\'', '""'], '', \substr($match[3], 1, \strlen($match[3]) - 2))), \stripcslashes(\substr($input, $cursor)), ]; } elseif (\preg_match('/'.StringInput::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) { $tokens[] = [ \stripcslashes(\substr($match[0], 1, \strlen($match[0]) - 2)), \stripcslashes(\substr($input, $cursor)), ]; } elseif (\preg_match('/'.StringInput::REGEX_STRING.'/A', $input, $match, 0, $cursor)) { $tokens[] = [ \stripcslashes($match[1]), \stripcslashes(\substr($input, $cursor)), ]; } else { // should never happen // @codeCoverageIgnoreStart throw new \InvalidArgumentException(\sprintf('Unable to parse input near "... %s ..."', \substr($input, $cursor, 10))); // @codeCoverageIgnoreEnd } $cursor += \strlen($match[0]); } return $tokens; } /** * Same as parent, but with some bonus handling for code arguments. */ protected function parse() { $parseOptions = true; $this->parsed = $this->tokenPairs; while (null !== $tokenPair = \array_shift($this->parsed)) { // token is what you'd expect. rest is the remainder of the input // string, including token, and will be used if this is a code arg. list($token, $rest) = $tokenPair; if ($parseOptions && '' === $token) { $this->parseShellArgument($token, $rest); } elseif ($parseOptions && '--' === $token) { $parseOptions = false; } elseif ($parseOptions && 0 === \strpos($token, '--')) { $this->parseLongOption($token); } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { $this->parseShortOption($token); } else { $this->parseShellArgument($token, $rest); } } } /** * Parses an argument, with bonus handling for code arguments. * * @param string $token The current token * @param string $rest The remaining unparsed input, including the current token * * @throws \RuntimeException When too many arguments are given */ private function parseShellArgument(string $token, string $rest) { $c = \count($this->arguments); // if input is expecting another argument, add it if ($this->definition->hasArgument($c)) { $arg = $this->definition->getArgument($c); if ($arg instanceof CodeArgument) { // When we find a code argument, we're done parsing. Add the // remaining input to the current argument and call it a day. $this->parsed = []; $this->arguments[$arg->getName()] = $rest; } else { $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; } return; } // (copypasta) // // @codeCoverageIgnoreStart // if last argument isArray(), append token to last argument if ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { $arg = $this->definition->getArgument($c - 1); $this->arguments[$arg->getName()][] = $token; return; } // unexpected argument $all = $this->definition->getArguments(); if (\count($all)) { throw new \RuntimeException(\sprintf('Too many arguments, expected arguments "%s".', \implode('" "', \array_keys($all)))); } throw new \RuntimeException(\sprintf('No arguments expected, got "%s".', $token)); // @codeCoverageIgnoreEnd } // Everything below this is copypasta from ArgvInput private methods // @codeCoverageIgnoreStart /** * Parses a short option. * * @param string $token The current token */ private function parseShortOption(string $token) { $name = \substr($token, 1); if (\strlen($name) > 1) { if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { // an option with a value (with no space) $this->addShortOption($name[0], \substr($name, 1)); } else { $this->parseShortOptionSet($name); } } else { $this->addShortOption($name, null); } } /** * Parses a short option set. * * @param string $name The current token * * @throws \RuntimeException When option given doesn't exist */ private function parseShortOptionSet(string $name) { $len = \strlen($name); for ($i = 0; $i < $len; $i++) { if (!$this->definition->hasShortcut($name[$i])) { throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $name[$i])); } $option = $this->definition->getOptionForShortcut($name[$i]); if ($option->acceptValue()) { $this->addLongOption($option->getName(), $i === $len - 1 ? null : \substr($name, $i + 1)); break; } else { $this->addLongOption($option->getName(), null); } } } /** * Parses a long option. * * @param string $token The current token */ private function parseLongOption(string $token) { $name = \substr($token, 2); if (false !== $pos = \strpos($name, '=')) { if (($value = \substr($name, $pos + 1)) === '') { \array_unshift($this->parsed, [$value, null]); } $this->addLongOption(\substr($name, 0, $pos), $value); } else { $this->addLongOption($name, null); } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws \RuntimeException When option given doesn't exist */ private function addShortOption(string $shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws \RuntimeException When option given doesn't exist */ private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { throw new \RuntimeException(\sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); if (null !== $value && !$option->acceptValue()) { throw new \RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name)); } if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = \array_shift($this->parsed); $nextToken = $next[0]; if ((isset($nextToken[0]) && '-' !== $nextToken[0]) || \in_array($nextToken, ['', null], true)) { $value = $nextToken; } else { \array_unshift($this->parsed, $next); } } if ($value === null) { if ($option->isValueRequired()) { throw new \RuntimeException(\sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isArray() && !$option->isValueOptional()) { $value = true; } } if ($option->isArray()) { $this->options[$name][] = $value; } else { $this->options[$name] = $value; } } // @codeCoverageIgnoreEnd } Input/CodeArgument.php000064400000003014150250565140010732 0ustar00validateInput($input); if (!$pattern = $input->getOption('grep')) { $this->filter = false; return; } if (!$this->stringIsRegex($pattern)) { $pattern = '/'.\preg_quote($pattern, '/').'/'; } if ($insensitive = $input->getOption('insensitive')) { $pattern .= 'i'; } $this->validateRegex($pattern); $this->filter = true; $this->pattern = $pattern; $this->insensitive = $insensitive; $this->invert = $input->getOption('invert'); } /** * Check whether the bound input has filter options. */ public function hasFilter(): bool { return $this->filter; } /** * Check whether a string matches the current filter options. * * @param string $string * @param array $matches */ public function match(string $string, array &$matches = null): bool { return $this->filter === false || (\preg_match($this->pattern, $string, $matches) xor $this->invert); } /** * Validate that grep, invert and insensitive input options are consistent. * * @throws RuntimeException if input is invalid * * @param InputInterface $input */ private function validateInput(InputInterface $input) { if (!$input->getOption('grep')) { foreach (['invert', 'insensitive'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense without --grep'); } } } } /** * Check whether a string appears to be a regular expression. * * @param string $string */ private function stringIsRegex(string $string): bool { return \substr($string, 0, 1) === '/' && \substr($string, -1) === '/' && \strlen($string) >= 3; } /** * Validate that $pattern is a valid regular expression. * * @throws RuntimeException if pattern is invalid * * @param string $pattern */ private function validateRegex(string $pattern) { \set_error_handler([ErrorException::class, 'throwException']); try { \preg_match($pattern, ''); } catch (ErrorException $e) { throw new RuntimeException(\str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage())); } finally { \restore_error_handler(); } } } Command/WtfCommand.php000064400000007414150250565140010703 0ustar00context = $context; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('wtf') ->setAliases(['last-exception', 'wtf?']) ->setDefinition([ new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show.'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show entire backtrace.'), $grep, $insensitive, $invert, ]) ->setDescription('Show the backtrace of the most recent exception.') ->setHelp( <<<'HELP' Shows a few lines of the backtrace of the most recent exception. If you want to see more lines, add more question marks or exclamation marks: e.g. >>> wtf ? >>> wtf ?!???!?!? To see the entire backtrace, pass the -a/--all flag: e.g. >>> wtf -a HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->filter->bind($input); $incredulity = \implode('', $input->getArgument('incredulity')); if (\strlen(\preg_replace('/[\\?!]/', '', $incredulity))) { throw new \InvalidArgumentException('Incredulity must include only "?" and "!"'); } $exception = $this->context->getLastException(); $count = $input->getOption('all') ? \PHP_INT_MAX : \max(3, \pow(2, \strlen($incredulity) + 1)); $shell = $this->getApplication(); if ($output instanceof ShellOutput) { $output->startPaging(); } do { $traceCount = \count($exception->getTrace()); $showLines = $count; // Show the whole trace if we'd only be hiding a few lines if ($traceCount < \max($count * 1.2, $count + 2)) { $showLines = \PHP_INT_MAX; } $trace = $this->getBacktrace($exception, $showLines); $moreLines = $traceCount - \count($trace); $output->writeln($shell->formatException($exception)); $output->writeln('--'); $output->write($trace, true, ShellOutput::NUMBER_LINES); $output->writeln(''); if ($moreLines > 0) { $output->writeln(\sprintf( '', $moreLines )); $output->writeln(''); } } while ($exception = $exception->getPrevious()); if ($output instanceof ShellOutput) { $output->stopPaging(); } return 0; } } Command/HistoryCommand.php000064400000017011150250565140011576 0ustar00filter = new FilterOptions(); parent::__construct($name); } /** * Set the Shell's Readline service. * * @param Readline $readline */ public function setReadline(Readline $readline) { $this->readline = $readline; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('history') ->setAliases(['hist']) ->setDefinition([ new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'), new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'), new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'), $grep, $insensitive, $invert, new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'), new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'), new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay.'), new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'), ]) ->setDescription('Show the Psy Shell history.') ->setHelp( <<<'HELP' Show, search, save or replay the Psy Shell history. e.g. >>> history --grep /[bB]acon/ >>> history --show 0..10 --replay >>> history --clear >>> history --tail 1000 --save somefile.txt HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateOnlyOne($input, ['show', 'head', 'tail']); $this->validateOnlyOne($input, ['save', 'replay', 'clear']); $history = $this->getHistorySlice( $input->getOption('show'), $input->getOption('head'), $input->getOption('tail') ); $highlighted = false; $this->filter->bind($input); if ($this->filter->hasFilter()) { $matches = []; $highlighted = []; foreach ($history as $i => $line) { if ($this->filter->match($line, $matches)) { if (isset($matches[0])) { $chunks = \explode($matches[0], $history[$i]); $chunks = \array_map([__CLASS__, 'escape'], $chunks); $glue = \sprintf('%s', self::escape($matches[0])); $highlighted[$i] = \implode($glue, $chunks); } } else { unset($history[$i]); } } } if ($save = $input->getOption('save')) { $output->writeln(\sprintf('Saving history in %s...', $save)); \file_put_contents($save, \implode(\PHP_EOL, $history).\PHP_EOL); $output->writeln('History saved.'); } elseif ($input->getOption('replay')) { if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) { throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying'); } $count = \count($history); $output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : '')); $this->getApplication()->addInput($history); } elseif ($input->getOption('clear')) { $this->clearHistory(); $output->writeln('History cleared.'); } else { $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES; if (!$highlighted) { $type = $type | OutputInterface::OUTPUT_RAW; } $output->page($highlighted ?: $history, $type); } return 0; } /** * Extract a range from a string. * * @param string $range * * @return array [ start, end ] */ private function extractRange(string $range): array { if (\preg_match('/^\d+$/', $range)) { return [$range, $range + 1]; } $matches = []; if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) { $start = $matches[1] ? (int) $matches[1] : 0; $end = $matches[2] ? (int) $matches[2] + 1 : \PHP_INT_MAX; return [$start, $end]; } throw new \InvalidArgumentException('Unexpected range: '.$range); } /** * Retrieve a slice of the readline history. * * @param string|null $show * @param string|null $head * @param string|null $tail * * @return array A slice of history */ private function getHistorySlice($show, $head, $tail): array { $history = $this->readline->listHistory(); // don't show the current `history` invocation \array_pop($history); if ($show) { list($start, $end) = $this->extractRange($show); $length = $end - $start; } elseif ($head) { if (!\preg_match('/^\d+$/', $head)) { throw new \InvalidArgumentException('Please specify an integer argument for --head'); } $start = 0; $length = (int) $head; } elseif ($tail) { if (!\preg_match('/^\d+$/', $tail)) { throw new \InvalidArgumentException('Please specify an integer argument for --tail'); } $start = \count($history) - $tail; $length = (int) $tail + 1; } else { return $history; } return \array_slice($history, $start, $length, true); } /** * Validate that only one of the given $options is set. * * @param InputInterface $input * @param array $options */ private function validateOnlyOne(InputInterface $input, array $options) { $count = 0; foreach ($options as $opt) { if ($input->getOption($opt)) { $count++; } } if ($count > 1) { throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options)); } } /** * Clear the readline history. */ private function clearHistory() { $this->readline->clearHistory(); } public static function escape(string $string): string { return OutputFormatter::escape($string); } } Command/TraceCommand.php000064400000005210150250565140011171 0ustar00filter = new FilterOptions(); parent::__construct($name); } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('trace') ->setDefinition([ new InputOption('include-psy', 'p', InputOption::VALUE_NONE, 'Include Psy in the call stack.'), new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Only include NUM lines.'), $grep, $insensitive, $invert, ]) ->setDescription('Show the current call stack.') ->setHelp( <<<'HELP' Show the current call stack. Optionally, include PsySH in the call stack by passing the --include-psy option. e.g. > trace -n10 > trace --include-psy HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->filter->bind($input); $trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy')); $output->page($trace, ShellOutput::NUMBER_LINES); return 0; } /** * Get a backtrace for an exception or error. * * Optionally limit the number of rows to include with $count, and exclude * Psy from the trace. * * @param \Throwable $e The exception or error with a backtrace * @param int $count (default: PHP_INT_MAX) * @param bool $includePsy (default: true) * * @return array Formatted stacktrace lines */ protected function getBacktrace(\Throwable $e, int $count = null, bool $includePsy = true): array { return TraceFormatter::formatTrace($e, $this->filter, $count, $includePsy); } } Command/HelpCommand.php000064400000005715150250565140011035 0ustar00setName('help') ->setAliases(['?']) ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name.', null), ]) ->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].') ->setHelp('My. How meta.'); } /** * Helper for setting a subcommand to retrieve help for. * * @param Command $command */ public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { if ($this->command !== null) { // help for an individual command $output->page($this->command->asText()); $this->command = null; } elseif ($name = $input->getArgument('command_name')) { // help for an individual command $output->page($this->getApplication()->get($name)->asText()); } else { // list available commands $commands = $this->getApplication()->all(); $table = $this->getTable($output); foreach ($commands as $name => $command) { if ($name !== $command->getName()) { continue; } if ($command->getAliases()) { $aliases = \sprintf('Aliases: %s', \implode(', ', $command->getAliases())); } else { $aliases = ''; } $table->addRow([ \sprintf('%s', $name), $command->getDescription(), $aliases, ]); } if ($output instanceof ShellOutput) { $output->startPaging(); } if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } if ($output instanceof ShellOutput) { $output->stopPaging(); } } return 0; } } Command/ExitCommand.php000064400000002201150250565140011041 0ustar00setName('exit') ->setAliases(['quit', 'q']) ->setDefinition([]) ->setDescription('End the current session and return to caller.') ->setHelp( <<<'HELP' End the current session and return to caller. e.g. >>> exit HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { throw new BreakException('Goodbye'); } } Command/ListCommand.php000064400000023476150250565140011064 0ustar00presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('ls') ->setAliases(['dir']) ->setDefinition([ new CodeArgument('target', CodeArgument::OPTIONAL, 'A target class or object to list.'), new InputOption('vars', '', InputOption::VALUE_NONE, 'Display variables.'), new InputOption('constants', 'c', InputOption::VALUE_NONE, 'Display defined constants.'), new InputOption('functions', 'f', InputOption::VALUE_NONE, 'Display defined functions.'), new InputOption('classes', 'k', InputOption::VALUE_NONE, 'Display declared classes.'), new InputOption('interfaces', 'I', InputOption::VALUE_NONE, 'Display declared interfaces.'), new InputOption('traits', 't', InputOption::VALUE_NONE, 'Display declared traits.'), new InputOption('no-inherit', '', InputOption::VALUE_NONE, 'Exclude inherited methods, properties and constants.'), new InputOption('properties', 'p', InputOption::VALUE_NONE, 'Display class or object properties (public properties by default).'), new InputOption('methods', 'm', InputOption::VALUE_NONE, 'Display class or object methods (public methods by default).'), $grep, $insensitive, $invert, new InputOption('globals', 'g', InputOption::VALUE_NONE, 'Include global variables.'), new InputOption('internal', 'n', InputOption::VALUE_NONE, 'Limit to internal functions and classes.'), new InputOption('user', 'u', InputOption::VALUE_NONE, 'Limit to user-defined constants, functions and classes.'), new InputOption('category', 'C', InputOption::VALUE_REQUIRED, 'Limit to constants in a specific category (e.g. "date").'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), new InputOption('long', 'l', InputOption::VALUE_NONE, 'List in long format: includes class names and method signatures.'), ]) ->setDescription('List local, instance or class variables, methods and constants.') ->setHelp( <<<'HELP' List variables, constants, classes, interfaces, traits, functions, methods, and properties. Called without options, this will return a list of variables currently in scope. If a target object is provided, list properties, constants and methods of that target. If a class, interface or trait name is passed instead, list constants and methods on that class. e.g. >>> ls >>> ls $foo >>> ls -k --grep mongo -i >>> ls -al ReflectionClass >>> ls --constants --category date >>> ls -l --functions --grep /^array_.*/ >>> ls -l --properties new DateTime() HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateInput($input); $this->initEnumerators(); $method = $input->getOption('long') ? 'writeLong' : 'write'; if ($target = $input->getArgument('target')) { list($target, $reflector) = $this->getTargetAndReflector($target); } else { $reflector = null; } // @todo something cleaner than this :-/ if ($output instanceof ShellOutput && $input->getOption('long')) { $output->startPaging(); } foreach ($this->enumerators as $enumerator) { $this->$method($output, $enumerator->enumerate($input, $reflector, $target)); } if ($output instanceof ShellOutput && $input->getOption('long')) { $output->stopPaging(); } // Set some magic local variables if ($reflector !== null) { $this->setCommandScopeVariables($reflector); } return 0; } /** * Initialize Enumerators. */ protected function initEnumerators() { if (!isset($this->enumerators)) { $mgr = $this->presenter; $this->enumerators = [ new ClassConstantEnumerator($mgr), new ClassEnumerator($mgr), new ConstantEnumerator($mgr), new FunctionEnumerator($mgr), new GlobalVariableEnumerator($mgr), new PropertyEnumerator($mgr), new MethodEnumerator($mgr), new VariableEnumerator($mgr, $this->context), ]; } } /** * Write the list items to $output. * * @param OutputInterface $output * @param array $result List of enumerated items */ protected function write(OutputInterface $output, array $result) { if (\count($result) === 0) { return; } foreach ($result as $label => $items) { $names = \array_map([$this, 'formatItemName'], $items); $output->writeln(\sprintf('%s: %s', $label, \implode(', ', $names))); } } /** * Write the list items to $output. * * Items are listed one per line, and include the item signature. * * @param OutputInterface $output * @param array $result List of enumerated items */ protected function writeLong(OutputInterface $output, array $result) { if (\count($result) === 0) { return; } $table = $this->getTable($output); foreach ($result as $label => $items) { $output->writeln(''); $output->writeln(\sprintf('%s:', $label)); $table->setRows([]); foreach ($items as $item) { $table->addRow([$this->formatItemName($item), $item['value']]); } if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } } } /** * Format an item name given its visibility. * * @param array $item */ private function formatItemName(array $item): string { return \sprintf('<%s>%s', $item['style'], OutputFormatter::escape($item['name']), $item['style']); } /** * Validate that input options make sense, provide defaults when called without options. * * @throws RuntimeException if options are inconsistent * * @param InputInterface $input */ private function validateInput(InputInterface $input) { if (!$input->getArgument('target')) { // if no target is passed, there can be no properties or methods foreach (['properties', 'methods', 'no-inherit'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense without a specified target'); } } foreach (['globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits'] as $option) { if ($input->getOption($option)) { return; } } // default to --vars if no other options are passed $input->setOption('vars', true); } else { // if a target is passed, classes, functions, etc don't make sense foreach (['vars', 'globals'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense with a specified target'); } } // @todo ensure that 'functions', 'classes', 'interfaces', 'traits' only accept namespace target? foreach (['constants', 'properties', 'methods', 'functions', 'classes', 'interfaces', 'traits'] as $option) { if ($input->getOption($option)) { return; } } // default to --constants --properties --methods if no other options are passed $input->setOption('constants', true); $input->setOption('properties', true); $input->setOption('methods', true); } } } Command/ClearCommand.php000064400000002150150250565140011161 0ustar00setName('clear') ->setDefinition([]) ->setDescription('Clear the Psy Shell screen.') ->setHelp( <<<'HELP' Clear the Psy Shell screen. Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too! HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $output->write(\sprintf('%c[2J%c[0;0f', 27, 27)); return 0; } } Command/WhereamiCommand.php000064400000010416150250565140011700 0ustar00backtrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('whereami') ->setDefinition([ new InputOption('num', 'n', InputOption::VALUE_OPTIONAL, 'Number of lines before and after.', '5'), new InputOption('file', 'f|a', InputOption::VALUE_NONE, 'Show the full source for the current file.'), ]) ->setDescription('Show where you are in the code.') ->setHelp( <<<'HELP' Show where you are in the code. Optionally, include the number of lines before and after you want to display, or --file for the whole file. e.g. > whereami > whereami -n10 > whereami --file HELP ); } /** * Obtains the correct stack frame in the full backtrace. * * @return array */ protected function trace(): array { foreach (\array_reverse($this->backtrace) as $stackFrame) { if ($this->isDebugCall($stackFrame)) { return $stackFrame; } } return \end($this->backtrace); } private static function isDebugCall(array $stackFrame): bool { $class = isset($stackFrame['class']) ? $stackFrame['class'] : null; $function = isset($stackFrame['function']) ? $stackFrame['function'] : null; return ($class === null && $function === 'Psy\\debug') || ($class === Shell::class && \in_array($function, ['__construct', 'debug'])); } /** * Determine the file and line based on the specific backtrace. * * @return array */ protected function fileInfo(): array { $stackFrame = $this->trace(); if (\preg_match('/eval\(/', $stackFrame['file'])) { \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches); $file = $matches[1][0]; $line = (int) $matches[2][0]; } else { $file = $stackFrame['file']; $line = $stackFrame['line']; } return \compact('file', 'line'); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $info = $this->fileInfo(); $num = $input->getOption('num'); $lineNum = $info['line']; $startLine = \max($lineNum - $num, 1); $endLine = $lineNum + $num; $code = \file_get_contents($info['file']); if ($input->getOption('file')) { $startLine = 1; $endLine = null; } if ($output instanceof ShellOutput) { $output->startPaging(); } $output->writeln(\sprintf('From %s:%s:', $this->replaceCwd($info['file']), $lineNum)); $output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $lineNum), false); if ($output instanceof ShellOutput) { $output->stopPaging(); } return 0; } /** * Replace the given directory from the start of a filepath. * * @param string $file */ private function replaceCwd(string $file): string { $cwd = \getcwd(); if ($cwd === false) { return $file; } $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } } Command/ParseCommand.php000064400000011263150250565140011212 0ustar00parserFactory = new ParserFactory(); $this->parsers = []; parent::__construct($name); } /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * PresenterAware interface. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter) { $this->presenter = clone $presenter; $this->presenter->addCasters([ Node::class => function (Node $node, array $a) { $a = [ Caster::PREFIX_VIRTUAL.'type' => $node->getType(), Caster::PREFIX_VIRTUAL.'attributes' => $node->getAttributes(), ]; foreach ($node->getSubNodeNames() as $name) { $a[Caster::PREFIX_VIRTUAL.$name] = $node->$name; } return $a; }, ]); } /** * {@inheritdoc} */ protected function configure() { $kindMsg = 'One of PhpParser\\ParserFactory constants: ' .\implode(', ', ParserFactory::getPossibleKinds()) ." (default is based on current interpreter's version)."; $this ->setName('parse') ->setDefinition([ new CodeArgument('code', CodeArgument::REQUIRED, 'PHP code to parse.'), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10), new InputOption('kind', '', InputOption::VALUE_REQUIRED, $kindMsg, $this->parserFactory->getDefaultKind()), ]) ->setDescription('Parse PHP code and show the abstract syntax tree.') ->setHelp( <<<'HELP' Parse PHP code and show the abstract syntax tree. This command is used in the development of PsySH. Given a string of PHP code, it pretty-prints the PHP Parser parse tree. See https://github.com/nikic/PHP-Parser It prolly won't be super useful for most of you, but it's here if you want to play. HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); if (\strpos($code, 'getOption('kind'); $depth = $input->getOption('depth'); $nodes = $this->parse($this->getParser($parserKind), $code); $output->page($this->presenter->present($nodes, $depth)); $this->context->setReturnValue($nodes); return 0; } /** * Lex and parse a string of code into statements. * * @param Parser $parser * @param string $code * * @return array Statements */ private function parse(Parser $parser, string $code): array { try { return $parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $parser->parse($code.';'); } } /** * Get (or create) the Parser instance. * * @param string|null $kind One of Psy\ParserFactory constants (only for PHP parser 2.0 and above) */ private function getParser(string $kind = null): Parser { if (!\array_key_exists($kind, $this->parsers)) { $this->parsers[$kind] = $this->parserFactory->createParser($kind); } return $this->parsers[$kind]; } } Command/SudoCommand.php000064400000007413150250565140011054 0ustar00parser = $parserFactory->createParser(); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor(new SudoVisitor()); $this->printer = new Printer(); parent::__construct($name); } /** * Set the Shell's Readline service. * * @param Readline $readline */ public function setReadline(Readline $readline) { $this->readline = $readline; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('sudo') ->setDefinition([ new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'), ]) ->setDescription('Evaluate PHP code, bypassing visibility restrictions.') ->setHelp( <<<'HELP' Evaluate PHP code, bypassing visibility restrictions. e.g. >>> $sekret->whisper("hi") PHP error: Call to private method Sekret::whisper() from context '' on line 1 >>> sudo $sekret->whisper("hi") => "hi" >>> $sekret->word PHP error: Cannot access private property Sekret::$word on line 1 >>> sudo $sekret->word => "hi" >>> $sekret->word = "please" PHP error: Cannot access private property Sekret::$word on line 1 >>> sudo $sekret->word = "please" => "please" HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); // special case for !! if ($code === '!!') { $history = $this->readline->listHistory(); if (\count($history) < 2) { throw new \InvalidArgumentException('No previous command to replay'); } $code = $history[\count($history) - 2]; } if (\strpos($code, 'traverser->traverse($this->parse($code)); $sudoCode = $this->printer->prettyPrint($nodes); $shell = $this->getApplication(); $shell->addCode($sudoCode, !$shell->hasCode()); return 0; } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private function parse(string $code): array { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } Command/BufferCommand.php000064400000004665150250565140011361 0ustar00setName('buffer') ->setAliases(['buf']) ->setDefinition([ new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'), ]) ->setDescription('Show (or clear) the contents of the code input buffer.') ->setHelp( <<<'HELP' Show the contents of the code buffer for the current multi-line expression. Optionally, clear the buffer by passing the --clear option. HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $app = $this->getApplication(); if (!$app instanceof \Psy\Shell) { throw new RuntimeException('Buffer command requires a \Psy\Shell application'); } $buf = $app->getCodeBuffer(); if ($input->getOption('clear')) { $app->resetCodeBuffer(); $output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES); } else { $output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES); } return 0; } /** * A helper method for wrapping buffer lines in `` and `` formatter strings. * * @param array $lines * @param string $type (default: 'return') * * @return array Formatted strings */ protected function formatLines(array $lines, string $type = 'return'): array { $template = \sprintf('<%s>%%s', $type, $type); return \array_map(function ($line) use ($template) { return \sprintf($template, $line); }, $lines); } } Command/Command.php000064400000017263150250565140010225 0ustar00Usage:', ' '.$this->getSynopsis(), '', ]; if ($this->getAliases()) { $messages[] = $this->aliasesAsText(); } if ($this->getArguments()) { $messages[] = $this->argumentsAsText(); } if ($this->getOptions()) { $messages[] = $this->optionsAsText(); } if ($help = $this->getProcessedHelp()) { $messages[] = 'Help:'; $messages[] = ' '.\str_replace("\n", "\n ", $help)."\n"; } return \implode("\n", $messages); } /** * {@inheritdoc} */ private function getArguments(): array { $hidden = $this->getHiddenArguments(); return \array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) { return !\in_array($argument->getName(), $hidden); }); } /** * These arguments will be excluded from help output. * * @return string[] */ protected function getHiddenArguments(): array { return ['command']; } /** * {@inheritdoc} */ private function getOptions(): array { $hidden = $this->getHiddenOptions(); return \array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) { return !\in_array($option->getName(), $hidden); }); } /** * These options will be excluded from help output. * * @return string[] */ protected function getHiddenOptions(): array { return ['verbose']; } /** * Format command aliases as text.. */ private function aliasesAsText(): string { return 'Aliases: '.\implode(', ', $this->getAliases()).''.\PHP_EOL; } /** * Format command arguments as text. */ private function argumentsAsText(): string { $max = $this->getMaxWidth(); $messages = []; $arguments = $this->getArguments(); if (!empty($arguments)) { $messages[] = 'Arguments:'; foreach ($arguments as $argument) { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { $default = \sprintf(' (default: %s)', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $argument->getDescription()); $messages[] = \sprintf(" %-{$max}s %s%s", $argument->getName(), $description, $default); } $messages[] = ''; } return \implode(\PHP_EOL, $messages); } /** * Format options as text. */ private function optionsAsText(): string { $max = $this->getMaxWidth(); $messages = []; $options = $this->getOptions(); if ($options) { $messages[] = 'Options:'; foreach ($options as $option) { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = \sprintf(' (default: %s)', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $multiple = $option->isArray() ? ' (multiple values allowed)' : ''; $description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $option->getDescription()); $optionMax = $max - \strlen($option->getName()) - 2; $messages[] = \sprintf( " %s %-{$optionMax}s%s%s%s", '--'.$option->getName(), $option->getShortcut() ? \sprintf('(-%s) ', $option->getShortcut()) : '', $description, $default, $multiple ); } $messages[] = ''; } return \implode(\PHP_EOL, $messages); } /** * Calculate the maximum padding width for a set of lines. */ private function getMaxWidth(): int { $max = 0; foreach ($this->getOptions() as $option) { $nameLength = \strlen($option->getName()) + 2; if ($option->getShortcut()) { $nameLength += \strlen($option->getShortcut()) + 3; } $max = \max($max, $nameLength); } foreach ($this->getArguments() as $argument) { $max = \max($max, \strlen($argument->getName())); } return ++$max; } /** * Format an option default as text. * * @param mixed $default */ private function formatDefaultValue($default): string { if (\is_array($default) && $default === \array_values($default)) { return \sprintf("['%s']", \implode("', '", $default)); } return \str_replace("\n", '', \var_export($default, true)); } /** * Get a Table instance. * * Falls back to legacy TableHelper. * * @return Table|TableHelper */ protected function getTable(OutputInterface $output) { if (!\class_exists(Table::class)) { return $this->getTableHelper(); } $style = new TableStyle(); // Symfony 4.1 deprecated single-argument style setters. if (\method_exists($style, 'setVerticalBorderChars')) { $style->setVerticalBorderChars(' '); $style->setHorizontalBorderChars(''); $style->setCrossingChars('', '', '', '', '', '', '', '', ''); } else { $style->setVerticalBorderChar(' '); $style->setHorizontalBorderChar(''); $style->setCrossingChar(''); } $table = new Table($output); return $table ->setRows([]) ->setStyle($style); } /** * Legacy fallback for getTable. */ protected function getTableHelper(): TableHelper { $table = $this->getApplication()->getHelperSet()->get('table'); return $table ->setRows([]) ->setLayout(TableHelper::LAYOUT_BORDERLESS) ->setHorizontalBorderChar('') ->setCrossingChar(''); } } Command/DocCommand.php000064400000022041150250565140010641 0ustar00setName('doc') ->setAliases(['rtfm', 'man']) ->setDefinition([ new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show documentation for superclasses as well as the current class.'), new CodeArgument('target', CodeArgument::REQUIRED, 'Function, class, instance, constant, method or property to document.'), ]) ->setDescription('Read the documentation for an object, class, constant, method or property.') ->setHelp( <<>>> doc preg_replace >>> doc Psy\Shell >>> doc Psy\Shell::debug >>> \$s = new Psy\Shell >>> doc \$s->run HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $value = $input->getArgument('target'); if (ReflectionLanguageConstruct::isLanguageConstruct($value)) { $reflector = new ReflectionLanguageConstruct($value); $doc = $this->getManualDocById($value); } else { list($target, $reflector) = $this->getTargetAndReflector($value); $doc = $this->getManualDoc($reflector) ?: DocblockFormatter::format($reflector); } $db = $this->getApplication()->getManualDb(); if ($output instanceof ShellOutput) { $output->startPaging(); } // Maybe include the declaring class if ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty) { $output->writeln(SignatureFormatter::format($reflector->getDeclaringClass())); } $output->writeln(SignatureFormatter::format($reflector)); $output->writeln(''); if (empty($doc) && !$db) { $output->writeln('PHP manual not found'); $output->writeln(' To document core PHP functionality, download the PHP reference manual:'); $output->writeln(' https://github.com/bobthecow/psysh/wiki/PHP-manual'); } else { $output->writeln($doc); } // Implicit --all if the original docblock has an {@inheritdoc} tag. if ($input->getOption('all') || \stripos($doc, self::INHERIT_DOC_TAG) !== false) { $parent = $reflector; foreach ($this->getParentReflectors($reflector) as $parent) { $output->writeln(''); $output->writeln('---'); $output->writeln(''); // Maybe include the declaring class if ($parent instanceof \ReflectionMethod || $parent instanceof \ReflectionProperty) { $output->writeln(SignatureFormatter::format($parent->getDeclaringClass())); } $output->writeln(SignatureFormatter::format($parent)); $output->writeln(''); if ($doc = $this->getManualDoc($parent) ?: DocblockFormatter::format($parent)) { $output->writeln($doc); } } } if ($output instanceof ShellOutput) { $output->stopPaging(); } // Set some magic local variables $this->setCommandScopeVariables($reflector); return 0; } private function getManualDoc($reflector) { switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: case \ReflectionFunction::class: $id = $reflector->name; break; case \ReflectionMethod::class: $id = $reflector->class.'::'.$reflector->name; break; case \ReflectionProperty::class: $id = $reflector->class.'::$'.$reflector->name; break; case \ReflectionClassConstant::class: case ReflectionClassConstant::class: // @todo this is going to collide with ReflectionMethod ids // someday... start running the query by id + type if the DB // supports it. $id = $reflector->class.'::'.$reflector->name; break; case ReflectionConstant_::class: $id = $reflector->name; break; default: return false; } return $this->getManualDocById($id); } /** * Get all all parent Reflectors for a given Reflector. * * For example, passing a Class, Object or TraitReflector will yield all * traits and parent classes. Passing a Method or PropertyReflector will * yield Reflectors for the same-named method or property on all traits and * parent classes. * * @return \Generator a whole bunch of \Reflector instances */ private function getParentReflectors($reflector): \Generator { $seenClasses = []; switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: foreach ($reflector->getTraits() as $trait) { if (!\in_array($trait->getName(), $seenClasses)) { $seenClasses[] = $trait->getName(); yield $trait; } } foreach ($reflector->getInterfaces() as $interface) { if (!\in_array($interface->getName(), $seenClasses)) { $seenClasses[] = $interface->getName(); yield $interface; } } while ($reflector = $reflector->getParentClass()) { yield $reflector; foreach ($reflector->getTraits() as $trait) { if (!\in_array($trait->getName(), $seenClasses)) { $seenClasses[] = $trait->getName(); yield $trait; } } foreach ($reflector->getInterfaces() as $interface) { if (!\in_array($interface->getName(), $seenClasses)) { $seenClasses[] = $interface->getName(); yield $interface; } } } return; case \ReflectionMethod::class: foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) { if ($parent->hasMethod($reflector->getName())) { $parentMethod = $parent->getMethod($reflector->getName()); if (!\in_array($parentMethod->getDeclaringClass()->getName(), $seenClasses)) { $seenClasses[] = $parentMethod->getDeclaringClass()->getName(); yield $parentMethod; } } } return; case \ReflectionProperty::class: foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) { if ($parent->hasProperty($reflector->getName())) { $parentProperty = $parent->getProperty($reflector->getName()); if (!\in_array($parentProperty->getDeclaringClass()->getName(), $seenClasses)) { $seenClasses[] = $parentProperty->getDeclaringClass()->getName(); yield $parentProperty; } } } break; } } private function getManualDocById($id) { if ($db = $this->getApplication()->getManualDb()) { $result = $db->query(\sprintf('SELECT doc FROM php_manual WHERE id = %s', $db->quote($id))); if ($result !== false) { return $result->fetchColumn(0); } } } } Command/DumpCommand.php000064400000005070150250565140011044 0ustar00presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('dump') ->setDefinition([ new CodeArgument('target', CodeArgument::REQUIRED, 'A target object or primitive to dump.'), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), ]) ->setDescription('Dump an object or primitive.') ->setHelp( <<<'HELP' Dump an object or primitive. This is like var_dump but way awesomer. e.g. >>> dump $_ >>> dump $someVar >>> dump $stuff->getAll() HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $depth = $input->getOption('depth'); $target = $this->resolveCode($input->getArgument('target')); $output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0)); if (\is_object($target)) { $this->setCommandScopeVariables(new \ReflectionObject($target)); } return 0; } /** * @deprecated Use `resolveCode` instead * * @param string $name * * @return mixed */ protected function resolveTarget(string $name) { @\trigger_error('`resolveTarget` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED); return $this->resolveCode($name); } } Command/ListCommand/PropertyEnumerator.php000064400000011536150250565140014744 0ustar00getOption('properties')) { return []; } $showAll = $input->getOption('all'); $noInherit = $input->getOption('no-inherit'); $properties = $this->prepareProperties($this->getProperties($showAll, $reflector, $noInherit), $target); if (empty($properties)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $properties; return $ret; } /** * Get defined properties for the given class or object Reflector. * * @param bool $showAll Include private and protected properties * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited properties * * @return array */ protected function getProperties(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $properties = []; foreach ($reflector->getProperties() as $property) { if ($noInherit && $property->getDeclaringClass()->getName() !== $className) { continue; } if ($showAll || $property->isPublic()) { $properties[$property->getName()] = $property; } } \ksort($properties, \SORT_NATURAL | \SORT_FLAG_CASE); return $properties; } /** * Prepare formatted property array. * * @param array $properties * * @return array */ protected function prepareProperties(array $properties, $target = null): array { // My kingdom for a generator. $ret = []; foreach ($properties as $name => $property) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => $this->getVisibilityStyle($property), 'value' => $this->presentValue($property, $target), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Properties'; } else { return 'Class Properties'; } } /** * Get output style for the given property's visibility. * * @param \ReflectionProperty $property */ private function getVisibilityStyle(\ReflectionProperty $property): string { if ($property->isPublic()) { return self::IS_PUBLIC; } elseif ($property->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } /** * Present the $target's current value for a reflection property. * * @param \ReflectionProperty $property * @param mixed $target */ protected function presentValue(\ReflectionProperty $property, $target): string { if (!$target) { return ''; } // If $target is a class or trait (try to) get the default // value for the property. if (!\is_object($target)) { try { $refl = new \ReflectionClass($target); $props = $refl->getDefaultProperties(); if (\array_key_exists($property->name, $props)) { $suffix = $property->isStatic() ? '' : ' '; return $this->presentRef($props[$property->name]).$suffix; } } catch (\Throwable $e) { // Well, we gave it a shot. } return ''; } $property->setAccessible(true); $value = $property->getValue($target); return $this->presentRef($value); } } Command/ListCommand/MethodEnumerator.php000064400000007616150250565140014344 0ustar00getOption('methods')) { return []; } $showAll = $input->getOption('all'); $noInherit = $input->getOption('no-inherit'); $methods = $this->prepareMethods($this->getMethods($showAll, $reflector, $noInherit)); if (empty($methods)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $methods; return $ret; } /** * Get defined methods for the given class or object Reflector. * * @param bool $showAll Include private and protected methods * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited methods * * @return array */ protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $methods = []; foreach ($reflector->getMethods() as $name => $method) { // For some reason PHP reflection shows private methods from the parent class, even // though they're effectively worthless. Let's suppress them here, like --no-inherit if (($noInherit || $method->isPrivate()) && $method->getDeclaringClass()->getName() !== $className) { continue; } if ($showAll || $method->isPublic()) { $methods[$method->getName()] = $method; } } \ksort($methods, \SORT_NATURAL | \SORT_FLAG_CASE); return $methods; } /** * Prepare formatted method array. * * @param array $methods * * @return array */ protected function prepareMethods(array $methods): array { // My kingdom for a generator. $ret = []; foreach ($methods as $name => $method) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => $this->getVisibilityStyle($method), 'value' => $this->presentSignature($method), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if ($reflector->isInterface()) { return 'Interface Methods'; } elseif (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Methods'; } else { return 'Class Methods'; } } /** * Get output style for the given method's visibility. * * @param \ReflectionMethod $method */ private function getVisibilityStyle(\ReflectionMethod $method): string { if ($method->isPublic()) { return self::IS_PUBLIC; } elseif ($method->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } } Command/ListCommand/ClassConstantEnumerator.php000064400000006256150250565140015702 0ustar00getOption('constants')) { return []; } $noInherit = $input->getOption('no-inherit'); $constants = $this->prepareConstants($this->getConstants($reflector, $noInherit)); if (empty($constants)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $constants; return $ret; } /** * Get defined constants for the given class or object Reflector. * * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited constants * * @return array */ protected function getConstants(\ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $constants = []; foreach ($reflector->getConstants() as $name => $constant) { $constReflector = ReflectionClassConstant::create($reflector->name, $name); if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) { continue; } $constants[$name] = $constReflector; } \ksort($constants, \SORT_NATURAL | \SORT_FLAG_CASE); return $constants; } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants): array { // My kingdom for a generator. $ret = []; foreach ($constants as $name => $constant) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constant->getValue()), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if ($reflector->isInterface()) { return 'Interface Constants'; } else { return 'Class Constants'; } } } Command/ListCommand/Enumerator.php000064400000005205150250565140013173 0ustar00filter = new FilterOptions(); $this->presenter = $presenter; } /** * Return a list of categorized things with the given input options and target. * * @param InputInterface $input * @param \Reflector|null $reflector * @param mixed $target * * @return array */ public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null): array { $this->filter->bind($input); return $this->listItems($input, $reflector, $target); } /** * Enumerate specific items with the given input options and target. * * Implementing classes should return an array of arrays: * * [ * 'Constants' => [ * 'FOO' => [ * 'name' => 'FOO', * 'style' => 'public', * 'value' => '123', * ], * ], * ] * * @param InputInterface $input * @param \Reflector|null $reflector * @param mixed $target * * @return array */ abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array; protected function showItem($name) { return $this->filter->match($name); } protected function presentRef($value) { return $this->presenter->presentRef($value); } protected function presentSignature($target) { // This might get weird if the signature is actually for a reflector. Hrm. if (!$target instanceof \Reflector) { $target = Mirror::get($target); } return SignatureFormatter::format($target); } } Command/ListCommand/FunctionEnumerator.php000064400000006004150250565140014677 0ustar00getOption('functions')) { return []; } if ($input->getOption('user')) { $label = 'User Functions'; $functions = $this->getFunctions('user'); } elseif ($input->getOption('internal')) { $label = 'Internal Functions'; $functions = $this->getFunctions('internal'); } else { $label = 'Functions'; $functions = $this->getFunctions(); } $prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\'; $functions = $this->prepareFunctions($functions, $prefix); if (empty($functions)) { return []; } $ret = []; $ret[$label] = $functions; return $ret; } /** * Get defined functions. * * Optionally limit functions to "user" or "internal" functions. * * @param string|null $type "user" or "internal" (default: both) * * @return array */ protected function getFunctions(string $type = null): array { $funcs = \get_defined_functions(); if ($type) { return $funcs[$type]; } else { return \array_merge($funcs['internal'], $funcs['user']); } } /** * Prepare formatted function array. * * @param array $functions * @param string $prefix * * @return array */ protected function prepareFunctions(array $functions, string $prefix = null): array { \natcasesort($functions); // My kingdom for a generator. $ret = []; foreach ($functions as $name) { if ($prefix !== null && \strpos(\strtolower($name), $prefix) !== 0) { continue; } if ($this->showItem($name)) { try { $ret[$name] = [ 'name' => $name, 'style' => self::IS_FUNCTION, 'value' => $this->presentSignature($name), ]; } catch (\Throwable $e) { // Ignore failures. } } } return $ret; } } Command/ListCommand/ClassEnumerator.php000064400000007426150250565140014170 0ustar00getOption('internal'); $user = $input->getOption('user'); $prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\'; $ret = []; // only list classes, interfaces and traits if we are specifically asked if ($input->getOption('classes')) { $ret = \array_merge($ret, $this->filterClasses('Classes', \get_declared_classes(), $internal, $user, $prefix)); } if ($input->getOption('interfaces')) { $ret = \array_merge($ret, $this->filterClasses('Interfaces', \get_declared_interfaces(), $internal, $user, $prefix)); } if ($input->getOption('traits')) { $ret = \array_merge($ret, $this->filterClasses('Traits', \get_declared_traits(), $internal, $user, $prefix)); } return \array_map([$this, 'prepareClasses'], \array_filter($ret)); } /** * Filter a list of classes, interfaces or traits. * * If $internal or $user is defined, results will be limited to internal or * user-defined classes as appropriate. * * @param string $key * @param array $classes * @param bool $internal * @param bool $user * @param string $prefix * * @return array */ protected function filterClasses(string $key, array $classes, bool $internal, bool $user, string $prefix = null): array { $ret = []; if ($internal) { $ret['Internal '.$key] = \array_filter($classes, function ($class) use ($prefix) { if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) { return false; } $refl = new \ReflectionClass($class); return $refl->isInternal(); }); } if ($user) { $ret['User '.$key] = \array_filter($classes, function ($class) use ($prefix) { if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) { return false; } $refl = new \ReflectionClass($class); return !$refl->isInternal(); }); } if (!$user && !$internal) { $ret[$key] = \array_filter($classes, function ($class) use ($prefix) { return $prefix === null || \strpos(\strtolower($class), $prefix) === 0; }); } return $ret; } /** * Prepare formatted class array. * * @param array $classes * * @return array */ protected function prepareClasses(array $classes): array { \natcasesort($classes); // My kingdom for a generator. $ret = []; foreach ($classes as $name) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CLASS, 'value' => $this->presentSignature($name), ]; } } return $ret; } } Command/ListCommand/ConstantEnumerator.php000064400000011211150250565140014677 0ustar00 'libxml', 'openssl' => 'OpenSSL', 'pcre' => 'PCRE', 'sqlite3' => 'SQLite3', 'curl' => 'cURL', 'dom' => 'DOM', 'ftp' => 'FTP', 'gd' => 'GD', 'gmp' => 'GMP', 'iconv' => 'iconv', 'json' => 'JSON', 'ldap' => 'LDAP', 'mbstring' => 'mbstring', 'odbc' => 'ODBC', 'pcntl' => 'PCNTL', 'pgsql' => 'pgsql', 'posix' => 'POSIX', 'mysqli' => 'mysqli', 'soap' => 'SOAP', 'exif' => 'EXIF', 'sysvmsg' => 'sysvmsg', 'xml' => 'XML', 'xsl' => 'XSL', ]; /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // if we have a reflector, ensure that it's a namespace reflector if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) { return []; } // only list constants if we are specifically asked if (!$input->getOption('constants')) { return []; } $user = $input->getOption('user'); $internal = $input->getOption('internal'); $category = $input->getOption('category'); if ($category) { $category = \strtolower($category); if ($category === 'internal') { $internal = true; $category = null; } elseif ($category === 'user') { $user = true; $category = null; } } $ret = []; if ($user) { $ret['User Constants'] = $this->getConstants('user'); } if ($internal) { $ret['Internal Constants'] = $this->getConstants('internal'); } if ($category) { $caseCategory = \array_key_exists($category, self::$categoryLabels) ? self::$categoryLabels[$category] : \ucfirst($category); $label = $caseCategory.' Constants'; $ret[$label] = $this->getConstants($category); } if (!$user && !$internal && !$category) { $ret['Constants'] = $this->getConstants(); } if ($reflector !== null) { $prefix = \strtolower($reflector->getName()).'\\'; foreach ($ret as $key => $names) { foreach (\array_keys($names) as $name) { if (\strpos(\strtolower($name), $prefix) !== 0) { unset($ret[$key][$name]); } } } } return \array_map([$this, 'prepareConstants'], \array_filter($ret)); } /** * Get defined constants. * * Optionally restrict constants to a given category, e.g. "date". If the * category is "internal", include all non-user-defined constants. * * @param string $category * * @return array */ protected function getConstants(string $category = null): array { if (!$category) { return \get_defined_constants(); } $consts = \get_defined_constants(true); if ($category === 'internal') { unset($consts['user']); return \array_merge(...\array_values($consts)); } foreach ($consts as $key => $value) { if (\strtolower($key) === $category) { return $value; } } return []; } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants): array { // My kingdom for a generator. $ret = []; $names = \array_keys($constants); \natcasesort($names); foreach ($names as $name) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constants[$name]), ]; } } return $ret; } } Command/ListCommand/VariableEnumerator.php000064400000006707150250565140014651 0ustar00context = $context; parent::__construct($presenter); } /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list variables when no Reflector is present. if ($reflector !== null || $target !== null) { return []; } // only list variables if we are specifically asked if (!$input->getOption('vars')) { return []; } $showAll = $input->getOption('all'); $variables = $this->prepareVariables($this->getVariables($showAll)); if (empty($variables)) { return []; } return [ 'Variables' => $variables, ]; } /** * Get scope variables. * * @param bool $showAll Include special variables (e.g. $_) * * @return array */ protected function getVariables(bool $showAll): array { $scopeVars = $this->context->getAll(); \uksort($scopeVars, function ($a, $b) { $aIndex = \array_search($a, self::$specialNames); $bIndex = \array_search($b, self::$specialNames); if ($aIndex !== false) { if ($bIndex !== false) { return $aIndex - $bIndex; } return 1; } if ($bIndex !== false) { return -1; } return \strnatcasecmp($a, $b); }); $ret = []; foreach ($scopeVars as $name => $val) { if (!$showAll && \in_array($name, self::$specialNames)) { continue; } $ret[$name] = $val; } return $ret; } /** * Prepare formatted variable array. * * @param array $variables * * @return array */ protected function prepareVariables(array $variables): array { // My kingdom for a generator. $ret = []; foreach ($variables as $name => $val) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => \in_array($name, self::$specialNames) ? self::IS_PRIVATE : self::IS_PUBLIC, 'value' => $this->presentRef($val), ]; } } return $ret; } } Command/ListCommand/GlobalVariableEnumerator.php000064400000003771150250565140015770 0ustar00getOption('globals')) { return []; } $globals = $this->prepareGlobals($this->getGlobals()); if (empty($globals)) { return []; } return [ 'Global Variables' => $globals, ]; } /** * Get defined global variables. * * @return array */ protected function getGlobals(): array { global $GLOBALS; $names = \array_keys($GLOBALS); \natcasesort($names); $ret = []; foreach ($names as $name) { $ret[$name] = $GLOBALS[$name]; } return $ret; } /** * Prepare formatted global variable array. * * @param array $globals * * @return array */ protected function prepareGlobals(array $globals): array { // My kingdom for a generator. $ret = []; foreach ($globals as $name => $value) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => self::IS_GLOBAL, 'value' => $this->presentRef($value), ]; } } return $ret; } } Command/PsyVersionCommand.php000064400000001671150250565140012263 0ustar00setName('version') ->setDefinition([]) ->setDescription('Show Psy Shell version.') ->setHelp('Show Psy Shell version.'); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln($this->getApplication()->getVersion()); return 0; } } Command/ThrowUpCommand.php000064400000010565150250565140011554 0ustar00parser = $parserFactory->createParser(); $this->printer = new Printer(); parent::__construct($name); } /** * @deprecated throwUp no longer needs to be ContextAware * * @param Context $context */ public function setContext(Context $context) { // Do nothing } /** * {@inheritdoc} */ protected function configure() { $this ->setName('throw-up') ->setDefinition([ new CodeArgument('exception', CodeArgument::OPTIONAL, 'Exception or Error to throw.'), ]) ->setDescription('Throw an exception or error out of the Psy Shell.') ->setHelp( <<<'HELP' Throws an exception or error out of the current the Psy Shell instance. By default it throws the most recent exception. e.g. >>> throw-up >>> throw-up $e >>> throw-up new Exception('WHEEEEEE!') >>> throw-up "bye!" HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code * * @throws \InvalidArgumentException if there is no exception to throw */ protected function execute(InputInterface $input, OutputInterface $output) { $args = $this->prepareArgs($input->getArgument('exception')); $throwStmt = new Throw_(new New_(new FullyQualifiedName(ThrowUpException::class), $args)); $throwCode = $this->printer->prettyPrint([$throwStmt]); $shell = $this->getApplication(); $shell->addCode($throwCode, !$shell->hasCode()); return 0; } /** * Parse the supplied command argument. * * If no argument was given, this falls back to `$_e` * * @throws \InvalidArgumentException if there is no exception to throw * * @param string $code * * @return Arg[] */ private function prepareArgs(string $code = null): array { if (!$code) { // Default to last exception if nothing else was supplied return [new Arg(new Variable('_e'))]; } if (\strpos($code, 'parse($code); if (\count($nodes) !== 1) { throw new \InvalidArgumentException('No idea how to throw this'); } $node = $nodes[0]; // Make this work for PHP Parser v3.x $expr = isset($node->expr) ? $node->expr : $node; $args = [new Arg($expr, false, false, $node->getAttributes())]; // Allow throwing via a string, e.g. `throw-up "SUP"` if ($expr instanceof String_) { return [new New_(new FullyQualifiedName(\Exception::class), $args)]; } return $args; } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private function parse(string $code): array { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } Command/EditCommand.php000064400000013343150250565140011026 0ustar00runtimeDir = $runtimeDir; } protected function configure() { $this ->setName('edit') ->setDefinition([ new InputArgument('file', InputArgument::OPTIONAL, 'The file to open for editing. If this is not given, edits a temporary file.', null), new InputOption( 'exec', 'e', InputOption::VALUE_NONE, 'Execute the file content after editing. This is the default when a file name argument is not given.', null ), new InputOption( 'no-exec', 'E', InputOption::VALUE_NONE, 'Do not execute the file content after editing. This is the default when a file name argument is given.', null ), ]) ->setDescription('Open an external editor. Afterwards, get produced code in input buffer.') ->setHelp('Set the EDITOR environment variable to something you\'d like to use.'); } /** * @param InputInterface $input * @param OutputInterface $output * * @return int 0 if everything went fine, or an exit code * * @throws \InvalidArgumentException when both exec and no-exec flags are given or if a given variable is not found in the current context * @throws \UnexpectedValueException if file_get_contents on the edited file returns false instead of a string */ protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('exec') && $input->getOption('no-exec')) { throw new \InvalidArgumentException('The --exec and --no-exec flags are mutually exclusive'); } $filePath = $this->extractFilePath($input->getArgument('file')); $execute = $this->shouldExecuteFile( $input->getOption('exec'), $input->getOption('no-exec'), $filePath ); $shouldRemoveFile = false; if ($filePath === null) { $filePath = \tempnam($this->runtimeDir, 'psysh-edit-command'); $shouldRemoveFile = true; } $editedContent = $this->editFile($filePath, $shouldRemoveFile); if ($execute) { $this->getApplication()->addInput($editedContent); } return 0; } /** * @param bool $execOption * @param bool $noExecOption * @param string|null $filePath */ private function shouldExecuteFile(bool $execOption, bool $noExecOption, string $filePath = null): bool { if ($execOption) { return true; } if ($noExecOption) { return false; } // By default, code that is edited is executed if there was no given input file path return $filePath === null; } /** * @param string|null $fileArgument * * @return string|null The file path to edit, null if the input was null, or the value of the referenced variable * * @throws \InvalidArgumentException If the variable is not found in the current context */ private function extractFilePath(string $fileArgument = null) { // If the file argument was a variable, get it from the context if ($fileArgument !== null && $fileArgument !== '' && $fileArgument[0] === '$') { $fileArgument = $this->context->get(\preg_replace('/^\$/', '', $fileArgument)); } return $fileArgument; } /** * @param string $filePath * @param bool $shouldRemoveFile * * @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string */ private function editFile(string $filePath, bool $shouldRemoveFile): string { $escapedFilePath = \escapeshellarg($filePath); $editor = (isset($_SERVER['EDITOR']) && $_SERVER['EDITOR']) ? $_SERVER['EDITOR'] : 'nano'; $pipes = []; $proc = \proc_open("{$editor} {$escapedFilePath}", [\STDIN, \STDOUT, \STDERR], $pipes); \proc_close($proc); $editedContent = @\file_get_contents($filePath); if ($shouldRemoveFile) { @\unlink($filePath); } if ($editedContent === false) { throw new \UnexpectedValueException("Reading {$filePath} returned false"); } return $editedContent; } /** * Set the Context reference. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } } Command/TimeitCommand.php000064400000012446150250565140011377 0ustar00Command took %.6f seconds to complete.'; const AVG_RESULT_MSG = 'Command took %.6f seconds on average (%.6f median; %.6f total) to complete.'; private static $start = null; private static $times = []; private $parser; private $traverser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { $parserFactory = new ParserFactory(); $this->parser = $parserFactory->createParser(); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor(new TimeitVisitor()); $this->printer = new Printer(); parent::__construct($name); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('timeit') ->setDefinition([ new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Number of iterations.'), new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'), ]) ->setDescription('Profiles with a timer.') ->setHelp( <<<'HELP' Time profiling for functions and commands. e.g. >>> timeit sleep(1) >>> timeit -n1000 $closure() HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); $num = $input->getOption('num') ?: 1; $shell = $this->getApplication(); $instrumentedCode = $this->instrumentCode($code); self::$times = []; for ($i = 0; $i < $num; $i++) { $_ = $shell->execute($instrumentedCode); $this->ensureEndMarked(); } $shell->writeReturnValue($_); $times = self::$times; self::$times = []; if ($num === 1) { $output->writeln(\sprintf(self::RESULT_MSG, $times[0])); } else { $total = \array_sum($times); \rsort($times); $median = $times[\round($num / 2)]; $output->writeln(\sprintf(self::AVG_RESULT_MSG, $total / $num, $median, $total)); } return 0; } /** * Internal method for marking the start of timeit execution. * * A static call to this method will be injected at the start of the timeit * input code to instrument the call. We will use the saved start time to * more accurately calculate time elapsed during execution. */ public static function markStart() { self::$start = \microtime(true); } /** * Internal method for marking the end of timeit execution. * * A static call to this method is injected by TimeitVisitor at the end * of the timeit input code to instrument the call. * * Note that this accepts an optional $ret parameter, which is used to pass * the return value of the last statement back out of timeit. This saves us * a bunch of code rewriting shenanigans. * * @param mixed $ret * * @return mixed it just passes $ret right back */ public static function markEnd($ret = null) { self::$times[] = \microtime(true) - self::$start; self::$start = null; return $ret; } /** * Ensure that the end of code execution was marked. * * The end *should* be marked in the instrumented code, but just in case * we'll add a fallback here. */ private function ensureEndMarked() { if (self::$start !== null) { self::markEnd(); } } /** * Instrument code for timeit execution. * * This inserts `markStart` and `markEnd` calls to ensure that (reasonably) * accurate times are recorded for just the code being executed. * * @param string $code */ private function instrumentCode(string $code): string { return $this->printer->prettyPrint($this->traverser->traverse($this->parse($code))); } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private function parse(string $code): array { $code = 'parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } Command/ShowCommand.php000064400000023221150250565140011055 0ustar00setName('show') ->setDefinition([ new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'), new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1), ]) ->setDescription('Show the code for an object, class, constant, method or property.') ->setHelp( <<cat --ex defaults to showing the lines surrounding the location of the last exception. Invoking it more than once travels up the exception's stack trace, and providing a number shows the context of the given index of the trace. e.g. >>> show \$myObject >>> show Psy\Shell::debug >>> show --ex >>> show --ex 3 HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { // n.b. As far as I can tell, InputInterface doesn't want to tell me // whether an option with an optional value was actually passed. If you // call `$input->getOption('ex')`, it will return the default, both when // `--ex` is specified with no value, and when `--ex` isn't specified at // all. // // So we're doing something sneaky here. If we call `getOptions`, it'll // return the default value when `--ex` is not present, and `null` if // `--ex` is passed with no value. /shrug $opts = $input->getOptions(); // Strict comparison to `1` (the default value) here, because `--ex 1` // will come in as `"1"`. Now we can tell the difference between // "no --ex present", because it's the integer 1, "--ex with no value", // because it's `null`, and "--ex 1", because it's the string "1". if ($opts['ex'] !== 1) { if ($input->getArgument('target')) { throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")'); } $this->writeExceptionContext($input, $output); return 0; } if ($input->getArgument('target')) { $this->writeCodeContext($input, $output); return 0; } throw new RuntimeException('Not enough arguments (missing: "target")'); } private function writeCodeContext(InputInterface $input, OutputInterface $output) { try { list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target')); } catch (UnexpectedTargetException $e) { // If we didn't get a target and Reflector, maybe we got a filename? $target = $e->getTarget(); if (\is_string($target) && \is_file($target) && $code = @\file_get_contents($target)) { $file = \realpath($target); if ($file !== $this->context->get('__file')) { $this->context->setCommandScopeVariables([ '__file' => $file, '__dir' => \dirname($file), ]); } $output->page(CodeFormatter::formatCode($code)); return; } else { throw $e; } } // Set some magic local variables $this->setCommandScopeVariables($reflector); try { $output->page(CodeFormatter::format($reflector)); } catch (RuntimeException $e) { $output->writeln(SignatureFormatter::format($reflector)); throw $e; } } private function writeExceptionContext(InputInterface $input, OutputInterface $output) { $exception = $this->context->getLastException(); if ($exception !== $this->lastException) { $this->lastException = null; $this->lastExceptionIndex = null; } $opts = $input->getOptions(); if ($opts['ex'] === null) { if ($this->lastException && $this->lastExceptionIndex !== null) { $index = $this->lastExceptionIndex + 1; } else { $index = 0; } } else { $index = \max(0, (int) $input->getOption('ex') - 1); } $trace = $exception->getTrace(); \array_unshift($trace, [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), ]); if ($index >= \count($trace)) { $index = 0; } $this->lastException = $exception; $this->lastExceptionIndex = $index; $output->writeln($this->getApplication()->formatException($exception)); $output->writeln('--'); $this->writeTraceLine($output, $trace, $index); $this->writeTraceCodeSnippet($output, $trace, $index); $this->setCommandScopeVariablesFromContext($trace[$index]); } private function writeTraceLine(OutputInterface $output, array $trace, $index) { $file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a'; $line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a'; $output->writeln(\sprintf( 'From %s:%d at level %d of backtrace (of %d):', OutputFormatter::escape($file), OutputFormatter::escape($line), $index + 1, \count($trace) )); } private function replaceCwd(string $file): string { if ($cwd = \getcwd()) { $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; } if ($cwd === false) { return $file; } else { return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } } private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index) { if (!isset($trace[$index]['file'])) { return; } $file = $trace[$index]['file']; if ($fileAndLine = $this->extractEvalFileAndLine($file)) { list($file, $line) = $fileAndLine; } else { if (!isset($trace[$index]['line'])) { return; } $line = $trace[$index]['line']; } if (\is_file($file)) { $code = @\file_get_contents($file); } if (empty($code)) { return; } $startLine = \max($line - 5, 0); $endLine = $line + 5; $output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $line), false); } private function setCommandScopeVariablesFromContext(array $context) { $vars = []; if (isset($context['class'])) { $vars['__class'] = $context['class']; if (isset($context['function'])) { $vars['__method'] = $context['function']; } try { $refl = new \ReflectionClass($context['class']); if ($namespace = $refl->getNamespaceName()) { $vars['__namespace'] = $namespace; } } catch (\Throwable $e) { // oh well } } elseif (isset($context['function'])) { $vars['__function'] = $context['function']; try { $refl = new \ReflectionFunction($context['function']); if ($namespace = $refl->getNamespaceName()) { $vars['__namespace'] = $namespace; } } catch (\Throwable $e) { // oh well } } if (isset($context['file'])) { $file = $context['file']; if ($fileAndLine = $this->extractEvalFileAndLine($file)) { list($file, $line) = $fileAndLine; } elseif (isset($context['line'])) { $line = $context['line']; } if (\is_file($file)) { $vars['__file'] = $file; if (isset($line)) { $vars['__line'] = $line; } $vars['__dir'] = \dirname($file); } } $this->context->setCommandScopeVariables($vars); } private function extractEvalFileAndLine(string $file) { if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) { return [$matches[1], $matches[2]]; } } } Command/TimeitCommand/TimeitVisitor.php000064400000010120150250565140014175 0ustar00functionDepth = 0; } /** * {@inheritdoc} * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { // keep track of nested function-like nodes, because they can have // returns statements... and we don't want to call markEnd for those. if ($node instanceof FunctionLike) { $this->functionDepth++; return; } // replace any top-level `return` statements with a `markEnd` call if ($this->functionDepth === 0 && $node instanceof Return_) { return new Return_($this->getEndCall($node->expr), $node->getAttributes()); } } /** * {@inheritdoc} * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth--; } } /** * {@inheritdoc} * * @return Node[]|null Array of nodes */ public function afterTraverse(array $nodes) { // prepend a `markStart` call \array_unshift($nodes, $this->maybeExpression($this->getStartCall())); // append a `markEnd` call (wrapping the final node, if it's an expression) $last = $nodes[\count($nodes) - 1]; if ($last instanceof Expr) { \array_pop($nodes); $nodes[] = $this->getEndCall($last); } elseif ($last instanceof Expression) { \array_pop($nodes); $nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes()); } elseif ($last instanceof Return_) { // nothing to do here, we're already ending with a return call } else { $nodes[] = $this->maybeExpression($this->getEndCall()); } return $nodes; } /** * Get PhpParser AST nodes for a `markStart` call. * * @return \PhpParser\Node\Expr\StaticCall */ private function getStartCall(): StaticCall { return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markStart'); } /** * Get PhpParser AST nodes for a `markEnd` call. * * Optionally pass in a return value. * * @param Expr|null $arg */ private function getEndCall(Expr $arg = null): StaticCall { if ($arg === null) { $arg = NoReturnValue::create(); } return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markEnd', [new Arg($arg)]); } /** * Compatibility shim for PHP Parser 3.x. * * Wrap $expr in a PhpParser\Node\Stmt\Expression if the class exists. * * @param \PhpParser\Node $expr * @param array $attrs * * @return \PhpParser\Node\Expr|\PhpParser\Node\Stmt\Expression */ private function maybeExpression(Node $expr, array $attrs = []) { return \class_exists(Expression::class) ? new Expression($expr, $attrs) : $expr; } } Command/ReflectingCommand.php000064400000024435150250565140012227 0ustar00)(\w+)$/'; /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get the target for a value. * * @throws \InvalidArgumentException when the value specified can't be resolved * * @param string $valueName Function, class, variable, constant, method or property name * * @return array (class or instance name, member name, kind) */ protected function getTarget(string $valueName): array { $valueName = \trim($valueName); $matches = []; switch (true) { case \preg_match(self::CLASS_OR_FUNC, $valueName, $matches): return [$this->resolveName($matches[0], true), null, 0]; case \preg_match(self::CLASS_MEMBER, $valueName, $matches): return [$this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD]; case \preg_match(self::CLASS_STATIC, $valueName, $matches): return [$this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY]; case \preg_match(self::INSTANCE_MEMBER, $valueName, $matches): if ($matches[2] === '->') { $kind = Mirror::METHOD | Mirror::PROPERTY; } else { $kind = Mirror::CONSTANT | Mirror::METHOD; } return [$this->resolveObject($matches[1]), $matches[3], $kind]; default: return [$this->resolveObject($valueName), null, 0]; } } /** * Resolve a class or function name (with the current shell namespace). * * @throws ErrorException when `self` or `static` is used in a non-class scope * * @param string $name * @param bool $includeFunctions (default: false) */ protected function resolveName(string $name, bool $includeFunctions = false): string { $shell = $this->getApplication(); // While not *technically* 100% accurate, let's treat `self` and `static` as equivalent. if (\in_array(\strtolower($name), ['self', 'static'])) { if ($boundClass = $shell->getBoundClass()) { return $boundClass; } if ($boundObject = $shell->getBoundObject()) { return \get_class($boundObject); } $msg = \sprintf('Cannot use "%s" when no class scope is active', \strtolower($name)); throw new ErrorException($msg, 0, \E_USER_ERROR, "eval()'d code", 1); } if (\substr($name, 0, 1) === '\\') { return $name; } // Check $name against the current namespace and use statements. if (self::couldBeClassName($name)) { try { $name = $this->resolveCode($name.'::class'); } catch (RuntimeException $e) { // /shrug } } if ($namespace = $shell->getNamespace()) { $fullName = $namespace.'\\'.$name; if (\class_exists($fullName) || \interface_exists($fullName) || ($includeFunctions && \function_exists($fullName))) { return $fullName; } } return $name; } /** * Check whether a given name could be a class name. */ protected function couldBeClassName(string $name): bool { // Regex based on https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class return \preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/', $name) === 1; } /** * Get a Reflector and documentation for a function, class or instance, constant, method or property. * * @param string $valueName Function, class, variable, constant, method or property name * * @return array (value, Reflector) */ protected function getTargetAndReflector(string $valueName): array { list($value, $member, $kind) = $this->getTarget($valueName); return [$value, Mirror::get($value, $member, $kind)]; } /** * Resolve code to a value in the current scope. * * @throws RuntimeException when the code does not return a value in the current scope * * @param string $code * * @return mixed Variable value */ protected function resolveCode(string $code) { try { $value = $this->getApplication()->execute($code, true); } catch (\Throwable $e) { // Swallow all exceptions? } if (!isset($value) || $value instanceof NoReturnValue) { throw new RuntimeException('Unknown target: '.$code); } return $value; } /** * Resolve code to an object in the current scope. * * @throws UnexpectedTargetException when the code resolves to a non-object value * * @param string $code * * @return object Variable instance */ private function resolveObject(string $code) { $value = $this->resolveCode($code); if (!\is_object($value)) { throw new UnexpectedTargetException($value, 'Unable to inspect a non-object'); } return $value; } /** * @deprecated Use `resolveCode` instead * * @param string $name * * @return mixed Variable instance */ protected function resolveInstance(string $name) { @\trigger_error('`resolveInstance` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED); return $this->resolveCode($name); } /** * Get a variable from the current shell scope. * * @param string $name * * @return mixed */ protected function getScopeVariable(string $name) { return $this->context->get($name); } /** * Get all scope variables from the current shell scope. * * @return array */ protected function getScopeVariables(): array { return $this->context->getAll(); } /** * Given a Reflector instance, set command-scope variables in the shell * execution context. This is used to inject magic $__class, $__method and * $__file variables (as well as a handful of others). * * @param \Reflector $reflector */ protected function setCommandScopeVariables(\Reflector $reflector) { $vars = []; switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: $vars['__class'] = $reflector->name; if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; case \ReflectionMethod::class: $vars['__method'] = \sprintf('%s::%s', $reflector->class, $reflector->name); $vars['__class'] = $reflector->class; $classReflector = $reflector->getDeclaringClass(); if ($classReflector->inNamespace()) { $vars['__namespace'] = $classReflector->getNamespaceName(); } break; case \ReflectionFunction::class: $vars['__function'] = $reflector->name; if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; case \ReflectionGenerator::class: $funcReflector = $reflector->getFunction(); $vars['__function'] = $funcReflector->name; if ($funcReflector->inNamespace()) { $vars['__namespace'] = $funcReflector->getNamespaceName(); } if ($fileName = $reflector->getExecutingFile()) { $vars['__file'] = $fileName; $vars['__line'] = $reflector->getExecutingLine(); $vars['__dir'] = \dirname($fileName); } break; case \ReflectionProperty::class: case \ReflectionClassConstant::class: case ReflectionClassConstant::class: $classReflector = $reflector->getDeclaringClass(); $vars['__class'] = $classReflector->name; if ($classReflector->inNamespace()) { $vars['__namespace'] = $classReflector->getNamespaceName(); } // no line for these, but this'll do if ($fileName = $reflector->getDeclaringClass()->getFileName()) { $vars['__file'] = $fileName; $vars['__dir'] = \dirname($fileName); } break; case ReflectionConstant_::class: if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; } if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) { if ($fileName = $reflector->getFileName()) { $vars['__file'] = $fileName; $vars['__line'] = $reflector->getStartLine(); $vars['__dir'] = \dirname($fileName); } } $this->context->setCommandScopeVariables($vars); } } Formatter/DocblockFormatter.php000064400000011060150250565140012625 0ustar00 'info', 'var' => 'strong', ]; /** * Format a docblock. * * @param \Reflector $reflector * * @return string Formatted docblock */ public static function format(\Reflector $reflector): string { $docblock = new Docblock($reflector); $chunks = []; if (!empty($docblock->desc)) { $chunks[] = 'Description:'; $chunks[] = self::indent(OutputFormatter::escape($docblock->desc), ' '); $chunks[] = ''; } if (!empty($docblock->tags)) { foreach ($docblock::$vectors as $name => $vector) { if (isset($docblock->tags[$name])) { $chunks[] = \sprintf('%s:', self::inflect($name)); $chunks[] = self::formatVector($vector, $docblock->tags[$name]); $chunks[] = ''; } } $tags = self::formatTags(\array_keys($docblock::$vectors), $docblock->tags); if (!empty($tags)) { $chunks[] = $tags; $chunks[] = ''; } } return \rtrim(\implode("\n", $chunks)); } /** * Format a docblock vector, for example, `@throws`, `@param`, or `@return`. * * @see DocBlock::$vectors * * @param array $vector * @param array $lines */ private static function formatVector(array $vector, array $lines): string { $template = [' ']; foreach ($vector as $type) { $max = 0; foreach ($lines as $line) { $chunk = $line[$type]; $cur = empty($chunk) ? 0 : \strlen($chunk) + 1; if ($cur > $max) { $max = $cur; } } $template[] = self::getVectorParamTemplate($type, $max); } $template = \implode(' ', $template); return \implode("\n", \array_map(function ($line) use ($template) { $escaped = \array_map(function ($l) { if ($l === null) { return ''; } return OutputFormatter::escape($l); }, $line); return \rtrim(\vsprintf($template, $escaped)); }, $lines)); } /** * Format docblock tags. * * @param array $skip Tags to exclude * @param array $tags Tags to format * * @return string formatted tags */ private static function formatTags(array $skip, array $tags): string { $chunks = []; foreach ($tags as $name => $values) { if (\in_array($name, $skip)) { continue; } foreach ($values as $value) { $chunks[] = \sprintf('%s%s %s', self::inflect($name), empty($value) ? '' : ':', OutputFormatter::escape($value)); } $chunks[] = ''; } return \implode("\n", $chunks); } /** * Get a docblock vector template. * * @param string $type Vector type * @param int $max Pad width */ private static function getVectorParamTemplate(string $type, int $max): string { if (!isset(self::$vectorParamTemplates[$type])) { return \sprintf('%%-%ds', $max); } return \sprintf('<%s>%%-%ds', self::$vectorParamTemplates[$type], $max, self::$vectorParamTemplates[$type]); } /** * Indent a string. * * @param string $text String to indent * @param string $indent (default: ' ') */ private static function indent(string $text, string $indent = ' '): string { return $indent.\str_replace("\n", "\n".$indent, $text); } /** * Convert underscored or whitespace separated words into sentence case. * * @param string $text */ private static function inflect(string $text): string { $words = \trim(\preg_replace('/[\s_-]+/', ' ', \preg_replace('/([a-z])([A-Z])/', '$1 $2', $text))); return \implode(' ', \array_map('ucfirst', \explode(' ', $words))); } } Formatter/CodeFormatter.php000064400000023532150250565140011766 0ustar00> '; const NO_LINE_MARKER = ' '; const HIGHLIGHT_DEFAULT = 'default'; const HIGHLIGHT_KEYWORD = 'keyword'; const HIGHLIGHT_PUBLIC = 'public'; const HIGHLIGHT_PROTECTED = 'protected'; const HIGHLIGHT_PRIVATE = 'private'; const HIGHLIGHT_CONST = 'const'; const HIGHLIGHT_NUMBER = 'number'; const HIGHLIGHT_STRING = 'string'; const HIGHLIGHT_COMMENT = 'comment'; const HIGHLIGHT_INLINE_HTML = 'inline_html'; private static $tokenMap = [ // Not highlighted \T_OPEN_TAG => self::HIGHLIGHT_DEFAULT, \T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT, \T_CLOSE_TAG => self::HIGHLIGHT_DEFAULT, \T_STRING => self::HIGHLIGHT_DEFAULT, \T_VARIABLE => self::HIGHLIGHT_DEFAULT, \T_NS_SEPARATOR => self::HIGHLIGHT_DEFAULT, // Visibility \T_PUBLIC => self::HIGHLIGHT_PUBLIC, \T_PROTECTED => self::HIGHLIGHT_PROTECTED, \T_PRIVATE => self::HIGHLIGHT_PRIVATE, // Constants \T_DIR => self::HIGHLIGHT_CONST, \T_FILE => self::HIGHLIGHT_CONST, \T_METHOD_C => self::HIGHLIGHT_CONST, \T_NS_C => self::HIGHLIGHT_CONST, \T_LINE => self::HIGHLIGHT_CONST, \T_CLASS_C => self::HIGHLIGHT_CONST, \T_FUNC_C => self::HIGHLIGHT_CONST, \T_TRAIT_C => self::HIGHLIGHT_CONST, // Types \T_DNUMBER => self::HIGHLIGHT_NUMBER, \T_LNUMBER => self::HIGHLIGHT_NUMBER, \T_ENCAPSED_AND_WHITESPACE => self::HIGHLIGHT_STRING, \T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING, // Comments \T_COMMENT => self::HIGHLIGHT_COMMENT, \T_DOC_COMMENT => self::HIGHLIGHT_COMMENT, // @todo something better here? \T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML, ]; /** * Format the code represented by $reflector for shell output. * * @param \Reflector $reflector * @param string|null $colorMode (deprecated and ignored) * * @return string formatted code */ public static function format(\Reflector $reflector, string $colorMode = null): string { if (self::isReflectable($reflector)) { if ($code = @\file_get_contents($reflector->getFileName())) { return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine()); } } throw new RuntimeException('Source code unavailable'); } /** * Format code for shell output. * * Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker. * * @param string $code * @param int $startLine * @param int|null $endLine * @param int|null $markLine * * @return string formatted code */ public static function formatCode(string $code, int $startLine = 1, int $endLine = null, int $markLine = null): string { $spans = self::tokenizeSpans($code); $lines = self::splitLines($spans, $startLine, $endLine); $lines = self::formatLines($lines); $lines = self::numberLines($lines, $markLine); return \implode('', \iterator_to_array($lines)); } /** * Get the start line for a given Reflector. * * Tries to incorporate doc comments if possible. * * This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already. * * @param \ReflectionClass|\ReflectionFunctionAbstract $reflector */ private static function getStartLine(\Reflector $reflector): int { $startLine = $reflector->getStartLine(); if ($docComment = $reflector->getDocComment()) { $startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1; } return \max($startLine, 1); } /** * Split code into highlight spans. * * Tokenize via \token_get_all, then map these tokens to internal highlight types, combining * adjacent spans of the same highlight type. * * @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point. * * @param string $code * * @return \Generator [$spanType, $spanText] highlight spans */ private static function tokenizeSpans(string $code): \Generator { $spanType = null; $buffer = ''; foreach (\token_get_all($code) as $token) { $nextType = self::nextHighlightType($token, $spanType); $spanType = $spanType ?: $nextType; if ($spanType !== $nextType) { yield [$spanType, $buffer]; $spanType = $nextType; $buffer = ''; } $buffer .= \is_array($token) ? $token[1] : $token; } if ($spanType !== null && $buffer !== '') { yield [$spanType, $buffer]; } } /** * Given a token and the current highlight span type, compute the next type. * * @param array|string $token \token_get_all token * @param string|null $currentType * * @return string|null */ private static function nextHighlightType($token, $currentType) { if ($token === '"') { return self::HIGHLIGHT_STRING; } if (\is_array($token)) { if ($token[0] === \T_WHITESPACE) { return $currentType; } if (\array_key_exists($token[0], self::$tokenMap)) { return self::$tokenMap[$token[0]]; } } return self::HIGHLIGHT_KEYWORD; } /** * Group highlight spans into an array of lines. * * Optionally, restrict by start and end line numbers. * * @param \Generator $spans as [$spanType, $spanText] pairs * @param int $startLine * @param int|null $endLine * * @return \Generator lines, each an array of [$spanType, $spanText] pairs */ private static function splitLines(\Generator $spans, int $startLine = 1, int $endLine = null): \Generator { $lineNum = 1; $buffer = []; foreach ($spans as list($spanType, $spanText)) { foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) { if ($index > 0) { if ($lineNum >= $startLine) { yield $lineNum => $buffer; } $lineNum++; $buffer = []; if ($endLine !== null && $lineNum > $endLine) { return; } } if ($spanLine !== '') { $buffer[] = [$spanType, $spanLine]; } } } if (!empty($buffer)) { yield $lineNum => $buffer; } } /** * Format lines of highlight spans for shell output. * * @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs * * @return \Generator Formatted lines */ private static function formatLines(\Generator $spanLines): \Generator { foreach ($spanLines as $lineNum => $spanLine) { $line = ''; foreach ($spanLine as list($spanType, $spanText)) { if ($spanType === self::HIGHLIGHT_DEFAULT) { $line .= OutputFormatter::escape($spanText); } else { $line .= \sprintf('<%s>%s', $spanType, OutputFormatter::escape($spanText), $spanType); } } yield $lineNum => $line.\PHP_EOL; } } /** * Prepend line numbers to formatted lines. * * Lines must be in an associative array with the correct keys in order to be numbered properly. * * Optionally, pass $markLine to add a line marker. * * @param \Generator $lines Formatted lines * @param int|null $markLine * * @return \Generator Numbered, formatted lines */ private static function numberLines(\Generator $lines, int $markLine = null): \Generator { $lines = \iterator_to_array($lines); // Figure out how much space to reserve for line numbers. \end($lines); $pad = \strlen(\key($lines)); // If $markLine is before or after our line range, don't bother reserving space for the marker. if ($markLine !== null) { if ($markLine > \key($lines)) { $markLine = null; } \reset($lines); if ($markLine < \key($lines)) { $markLine = null; } } foreach ($lines as $lineNum => $line) { $mark = ''; if ($markLine !== null) { $mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER; } yield \sprintf("%s: %s", $mark, $lineNum, $line); } } /** * Check whether a Reflector instance is reflectable by this formatter. * * @phpstan-assert-if-true \ReflectionClass|\ReflectionFunctionAbstract $reflector * * @param \Reflector $reflector */ private static function isReflectable(\Reflector $reflector): bool { return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName()); } } Formatter/ReflectorFormatter.php000064400000000662150250565140013040 0ustar00getName(); } /** * Print the method, property or class modifiers. * * @param \ReflectionMethod|\ReflectionProperty|\ReflectionClass $reflector * * @return string Formatted modifiers */ private static function formatModifiers(\Reflector $reflector): string { return \implode(' ', \array_map(function ($modifier) { return \sprintf('%s', $modifier); }, \Reflection::getModifierNames($reflector->getModifiers()))); } /** * Format a class signature. * * @param \ReflectionClass $reflector * * @return string Formatted signature */ private static function formatClass(\ReflectionClass $reflector): string { $chunks = []; if ($modifiers = self::formatModifiers($reflector)) { $chunks[] = $modifiers; } if ($reflector->isTrait()) { $chunks[] = 'trait'; } else { $chunks[] = $reflector->isInterface() ? 'interface' : 'class'; } $chunks[] = \sprintf('%s', self::formatName($reflector)); if ($parent = $reflector->getParentClass()) { $chunks[] = 'extends'; $chunks[] = \sprintf('%s', $parent->getName()); } $interfaces = $reflector->getInterfaceNames(); if (!empty($interfaces)) { \sort($interfaces); $chunks[] = $reflector->isInterface() ? 'extends' : 'implements'; $chunks[] = \implode(', ', \array_map(function ($name) { return \sprintf('%s', $name); }, $interfaces)); } return \implode(' ', $chunks); } /** * Format a constant signature. * * @param ReflectionClassConstant|\ReflectionClassConstant $reflector * * @return string Formatted signature */ private static function formatClassConstant($reflector): string { $value = $reflector->getValue(); $style = self::getTypeStyle($value); return \sprintf( 'const %s = <%s>%s', self::formatName($reflector), $style, OutputFormatter::escape(Json::encode($value)), $style ); } /** * Format a constant signature. * * @param ReflectionConstant_ $reflector * * @return string Formatted signature */ private static function formatConstant(ReflectionConstant_ $reflector): string { $value = $reflector->getValue(); $style = self::getTypeStyle($value); return \sprintf( 'define(%s, <%s>%s)', OutputFormatter::escape(Json::encode($reflector->getName())), $style, OutputFormatter::escape(Json::encode($value)), $style ); } /** * Helper for getting output style for a given value's type. * * @param mixed $value */ private static function getTypeStyle($value): string { if (\is_int($value) || \is_float($value)) { return 'number'; } elseif (\is_string($value)) { return 'string'; } elseif (\is_bool($value) || $value === null) { return 'bool'; } else { return 'strong'; // @codeCoverageIgnore } } /** * Format a property signature. * * @param \ReflectionProperty $reflector * * @return string Formatted signature */ private static function formatProperty(\ReflectionProperty $reflector): string { return \sprintf( '%s $%s', self::formatModifiers($reflector), $reflector->getName() ); } /** * Format a function signature. * * @param \ReflectionFunction $reflector * * @return string Formatted signature */ private static function formatFunction(\ReflectionFunctionAbstract $reflector): string { return \sprintf( 'function %s%s(%s)%s', $reflector->returnsReference() ? '&' : '', self::formatName($reflector), \implode(', ', self::formatFunctionParams($reflector)), self::formatFunctionReturnType($reflector) ); } /** * Format a function signature's return type (if available). * * @param \ReflectionFunctionAbstract $reflector * * @return string Formatted return type */ private static function formatFunctionReturnType(\ReflectionFunctionAbstract $reflector): string { if (!\method_exists($reflector, 'hasReturnType') || !$reflector->hasReturnType()) { return ''; } return \sprintf(': %s', self::formatReflectionType($reflector->getReturnType())); } /** * Format a method signature. * * @param \ReflectionMethod $reflector * * @return string Formatted signature */ private static function formatMethod(\ReflectionMethod $reflector): string { return \sprintf( '%s %s', self::formatModifiers($reflector), self::formatFunction($reflector) ); } /** * Print the function params. * * @param \ReflectionFunctionAbstract $reflector * * @return array */ private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector): array { $params = []; foreach ($reflector->getParameters() as $param) { $hint = ''; try { if (\method_exists($param, 'getType')) { $hint = self::formatReflectionType($param->getType()); } else { if ($param->isArray()) { $hint = 'array'; } elseif ($class = $param->getClass()) { $hint = \sprintf('%s', $class->getName()); } } } catch (\Throwable $e) { // sometimes we just don't know... // bad class names, or autoloaded classes that haven't been loaded yet, or whathaveyou. // come to think of it, the only time I've seen this is with the intl extension. // Hax: we'll try to extract it :P // @codeCoverageIgnoreStart $chunks = \explode('$'.$param->getName(), (string) $param); $chunks = \explode(' ', \trim($chunks[0])); $guess = \end($chunks); $hint = \sprintf('%s', OutputFormatter::escape($guess)); // @codeCoverageIgnoreEnd } if ($param->isOptional()) { if (!$param->isDefaultValueAvailable()) { $value = 'unknown'; $typeStyle = 'urgent'; } else { $value = $param->getDefaultValue(); $typeStyle = self::getTypeStyle($value); $value = \is_array($value) ? '[]' : ($value === null ? 'null' : \var_export($value, true)); } $default = \sprintf(' = <%s>%s', $typeStyle, OutputFormatter::escape($value), $typeStyle); } else { $default = ''; } $params[] = \sprintf( '%s%s%s$%s%s', $param->isPassedByReference() ? '&' : '', $hint, $hint !== '' ? ' ' : '', $param->getName(), $default ); } return $params; } /** * Print function param or return type(s). * * @param \ReflectionType $type */ private static function formatReflectionType(\ReflectionType $type = null): string { if ($type === null) { return ''; } $types = $type instanceof \ReflectionUnionType ? $type->getTypes() : [$type]; $formattedTypes = []; foreach ($types as $type) { $typeStyle = $type->isBuiltin() ? 'keyword' : 'class'; // PHP 7.0 didn't have `getName` on reflection types, so wheee! $typeName = \method_exists($type, 'getName') ? $type->getName() : (string) $type; // @todo Do we want to include the ? for nullable types? Maybe only sometimes? $formattedTypes[] = \sprintf('<%s>%s', $typeStyle, OutputFormatter::escape($typeName), $typeStyle); } return \implode('|', $formattedTypes); } } Formatter/Formatter.php000064400000000646150250565140011174 0ustar00getTrace(); \array_unshift($trace, [ 'function' => '', 'file' => $throwable->getFile() !== null ? $throwable->getFile() : 'n/a', 'line' => $throwable->getLine() !== null ? $throwable->getLine() : 'n/a', 'args' => [], ]); if (!$includePsy) { for ($i = \count($trace) - 1; $i >= 0; $i--) { $thing = isset($trace[$i]['class']) ? $trace[$i]['class'] : $trace[$i]['function']; if (\preg_match('/\\\\?Psy\\\\/', $thing)) { $trace = \array_slice($trace, $i + 1); break; } } } for ($i = 0, $count = \min($count, \count($trace)); $i < $count; $i++) { $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; $function = $trace[$i]['function']; $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; // Make file paths relative to cwd if ($cwd !== false) { $file = \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } // Leave execution loop out of the `eval()'d code` lines if (\preg_match("#/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code$#", \str_replace('\\', '/', $file))) { $file = "eval()'d code"; } // Skip any lines that don't match our filter options if ($filter !== null && !$filter->match(\sprintf('%s%s%s() at %s:%s', $class, $type, $function, $file, $line))) { continue; } $lines[] = \sprintf( ' %s%s%s() at %s:%s', OutputFormatter::escape($class), OutputFormatter::escape($type), OutputFormatter::escape($function), OutputFormatter::escape($file), OutputFormatter::escape($line) ); } return $lines; } } Configuration.php000064400000154542150250565140010102 0ustar00configPaths = new ConfigPaths(); // explicit configFile option if (isset($config['configFile'])) { $this->configFile = $config['configFile']; } elseif (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) { $this->configFile = $_SERVER['PSYSH_CONFIG']; } // legacy baseDir option if (isset($config['baseDir'])) { $msg = "The 'baseDir' configuration option is deprecated; ". "please specify 'configDir' and 'dataDir' options instead"; throw new DeprecatedException($msg); } unset($config['configFile'], $config['baseDir']); // go go gadget, config! $this->loadConfig($config); $this->init(); } /** * Construct a Configuration object from Symfony Console input. * * This is great for adding psysh-compatible command line options to framework- or app-specific * wrappers. * * $input should already be bound to an appropriate InputDefinition (see self::getInputOptions * if you want to build your own) before calling this method. It's not required, but things work * a lot better if we do. * * @see self::getInputOptions * * @throws \InvalidArgumentException * * @param InputInterface $input */ public static function fromInput(InputInterface $input): self { $config = new self(['configFile' => self::getConfigFileFromInput($input)]); // Handle --color and --no-color (and --ansi and --no-ansi aliases) if (self::getOptionFromInput($input, ['color', 'ansi'])) { $config->setColorMode(self::COLOR_MODE_FORCED); } elseif (self::getOptionFromInput($input, ['no-color', 'no-ansi'])) { $config->setColorMode(self::COLOR_MODE_DISABLED); } // Handle verbosity options if ($verbosity = self::getVerbosityFromInput($input)) { $config->setVerbosity($verbosity); } // Handle interactive mode if (self::getOptionFromInput($input, ['interactive', 'interaction'], ['-a', '-i'])) { $config->setInteractiveMode(self::INTERACTIVE_MODE_FORCED); } elseif (self::getOptionFromInput($input, ['no-interactive', 'no-interaction'], ['-n'])) { $config->setInteractiveMode(self::INTERACTIVE_MODE_DISABLED); } // Handle --compact if (self::getOptionFromInput($input, ['compact'])) { $config->setTheme('compact'); } // Handle --raw-output // @todo support raw output with interactive input? if (!$config->getInputInteractive()) { if (self::getOptionFromInput($input, ['raw-output'], ['-r'])) { $config->setRawOutput(true); } } // Handle --yolo if (self::getOptionFromInput($input, ['yolo'])) { $config->setYolo(true); } return $config; } /** * Get the desired config file from the given input. * * @return string|null config file path, or null if none is specified */ private static function getConfigFileFromInput(InputInterface $input) { // Best case, input is properly bound and validated. if ($input->hasOption('config')) { return $input->getOption('config'); } return $input->getParameterOption('--config', null, true) ?: $input->getParameterOption('-c', null, true); } /** * Get a boolean option from the given input. * * This helper allows fallback for unbound and unvalidated input. It's not perfect--for example, * it can't deal with several short options squished together--but it's better than falling over * any time someone gives us unbound input. * * @return bool true if the option (or an alias) is present */ private static function getOptionFromInput(InputInterface $input, array $names, array $otherParams = []): bool { // Best case, input is properly bound and validated. foreach ($names as $name) { if ($input->hasOption($name) && $input->getOption($name)) { return true; } } foreach ($names as $name) { $otherParams[] = '--'.$name; } foreach ($otherParams as $name) { if ($input->hasParameterOption($name, true)) { return true; } } return false; } /** * Get the desired verbosity from the given input. * * This is a bit more complext than the other options parsers. It handles `--quiet` and * `--verbose`, along with their short aliases, and fancy things like `-vvv`. * * @return string|null configuration constant, or null if no verbosity option is specified */ private static function getVerbosityFromInput(InputInterface $input) { // --quiet wins! if (self::getOptionFromInput($input, ['quiet'], ['-q'])) { return self::VERBOSITY_QUIET; } // Best case, input is properly bound and validated. // // Note that if the `--verbose` option is incorrectly defined as `VALUE_NONE` rather than // `VALUE_OPTIONAL` (as it is in Symfony Console by default) it doesn't actually work with // multiple verbosity levels as it claims. // // We can detect this by checking whether the the value === true, and fall back to unbound // parsing for this option. if ($input->hasOption('verbose') && $input->getOption('verbose') !== true) { switch ($input->getOption('verbose')) { case '-1': return self::VERBOSITY_QUIET; case '0': // explicitly normal, overrides config file default return self::VERBOSITY_NORMAL; case '1': case null: // `--verbose` and `-v` return self::VERBOSITY_VERBOSE; case '2': case 'v': // `-vv` return self::VERBOSITY_VERY_VERBOSE; case '3': case 'vv': // `-vvv` return self::VERBOSITY_DEBUG; default: // implicitly normal, config file default wins return; } } // quiet and normal have to come before verbose, because it eats everything else. if ($input->hasParameterOption('--verbose=-1', true) || $input->getParameterOption('--verbose', false, true) === '-1') { return self::VERBOSITY_QUIET; } if ($input->hasParameterOption('--verbose=0', true) || $input->getParameterOption('--verbose', false, true) === '0') { return self::VERBOSITY_NORMAL; } // `-vvv`, `-vv` and `-v` have to come in descending length order, because `hasParameterOption` matches prefixes. if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || $input->getParameterOption('--verbose', false, true) === '3') { return self::VERBOSITY_DEBUG; } if ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || $input->getParameterOption('--verbose', false, true) === '2') { return self::VERBOSITY_VERY_VERBOSE; } if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true)) { return self::VERBOSITY_VERBOSE; } } /** * Get a list of input options expected when initializing Configuration via input. * * @see self::fromInput * * @return InputOption[] */ public static function getInputOptions(): array { return [ new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use an alternate PsySH config file location.'), new InputOption('cwd', null, InputOption::VALUE_REQUIRED, 'Use an alternate working directory.'), new InputOption('color', null, InputOption::VALUE_NONE, 'Force colors in output.'), new InputOption('no-color', null, InputOption::VALUE_NONE, 'Disable colors in output.'), // --ansi and --no-ansi aliases to match Symfony, Composer, etc. new InputOption('ansi', null, InputOption::VALUE_NONE, 'Force colors in output.'), new InputOption('no-ansi', null, InputOption::VALUE_NONE, 'Disable colors in output.'), new InputOption('quiet', 'q', InputOption::VALUE_NONE, 'Shhhhhh.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_OPTIONAL, 'Increase the verbosity of messages.', '0'), new InputOption('compact', null, InputOption::VALUE_NONE, 'Run PsySH with compact output.'), new InputOption('interactive', 'i|a', InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'), new InputOption('no-interactive', 'n', InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'), // --interaction and --no-interaction aliases for compatibility with Symfony, Composer, etc new InputOption('interaction', null, InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'), new InputOption('no-interaction', null, InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'), new InputOption('raw-output', 'r', InputOption::VALUE_NONE, 'Print var_export-style return values (for non-interactive input)'), new InputOption('self-update', 'u', InputOption::VALUE_NONE, 'Update to the latest version'), new InputOption('yolo', null, InputOption::VALUE_NONE, 'Run PsySH with minimal input validation. You probably don\'t want this.'), ]; } /** * Initialize the configuration. * * This checks for the presence of Readline and Pcntl extensions. * * If a config file is available, it will be loaded and merged with the current config. * * If no custom config file was specified and a local project config file * is available, it will be loaded and merged with the current config. */ public function init() { // feature detection $this->hasReadline = \function_exists('readline'); $this->hasPcntl = ProcessForker::isSupported(); if ($configFile = $this->getConfigFile()) { $this->loadConfigFile($configFile); } if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) { $this->loadConfigFile($localConfig); } $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current PsySH config file. * * If a `configFile` option was passed to the Configuration constructor, * this file will be returned. If not, all possible config directories will * be searched, and the first `config.php` or `rc.php` file which exists * will be returned. * * If you're trying to decide where to put your config file, pick * * ~/.config/psysh/config.php * * @return string|null */ public function getConfigFile() { if (isset($this->configFile)) { return $this->configFile; } $files = $this->configPaths->configFiles(['config.php', 'rc.php']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple configuration files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } return $files[0]; } } /** * Get the local PsySH config file. * * Searches for a project specific config file `.psysh.php` in the current * working directory. * * @return string|null */ public function getLocalConfigFile() { $localConfig = \getcwd().'/.psysh.php'; if (@\is_file($localConfig)) { return $localConfig; } } /** * Load configuration values from an array of options. * * @param array $options */ public function loadConfig(array $options) { foreach (self::$AVAILABLE_OPTIONS as $option) { if (isset($options[$option])) { $method = 'set'.\ucfirst($option); $this->$method($options[$option]); } } // legacy `tabCompletion` option if (isset($options['tabCompletion'])) { $msg = '`tabCompletion` is deprecated; use `useTabCompletion` instead.'; @\trigger_error($msg, \E_USER_DEPRECATED); $this->setUseTabCompletion($options['tabCompletion']); } foreach (['commands', 'matchers', 'casters'] as $option) { if (isset($options[$option])) { $method = 'add'.\ucfirst($option); $this->$method($options[$option]); } } // legacy `tabCompletionMatchers` option if (isset($options['tabCompletionMatchers'])) { $msg = '`tabCompletionMatchers` is deprecated; use `matchers` instead.'; @\trigger_error($msg, \E_USER_DEPRECATED); $this->addMatchers($options['tabCompletionMatchers']); } } /** * Load a configuration file (default: `$HOME/.config/psysh/config.php`). * * This configuration instance will be available to the config file as $config. * The config file may directly manipulate the configuration, or may return * an array of options which will be merged with the current configuration. * * @throws \InvalidArgumentException if the config file does not exist or returns a non-array result * * @param string $file */ public function loadConfigFile(string $file) { if (!\is_file($file)) { throw new \InvalidArgumentException(\sprintf('Invalid configuration file specified, %s does not exist', $file)); } $__psysh_config_file__ = $file; $load = function ($config) use ($__psysh_config_file__) { $result = require $__psysh_config_file__; if ($result !== 1) { return $result; } }; $result = $load($this); if (!empty($result)) { if (\is_array($result)) { $this->loadConfig($result); } else { throw new \InvalidArgumentException('Psy Shell configuration must return an array of options'); } } } /** * Set files to be included by default at the start of each shell session. * * @param array $includes */ public function setDefaultIncludes(array $includes = []) { $this->defaultIncludes = $includes; } /** * Get files to be included by default at the start of each shell session. * * @return string[] */ public function getDefaultIncludes(): array { return $this->defaultIncludes ?: []; } /** * Set the shell's config directory location. * * @param string $dir */ public function setConfigDir(string $dir) { $this->configDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current configuration directory, if any is explicitly set. * * @return string|null */ public function getConfigDir() { return $this->configDir; } /** * Set the shell's data directory location. * * @param string $dir */ public function setDataDir(string $dir) { $this->dataDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current data directory, if any is explicitly set. * * @return string|null */ public function getDataDir() { return $this->dataDir; } /** * Set the shell's temporary directory location. * * @param string $dir */ public function setRuntimeDir(string $dir) { $this->runtimeDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the shell's temporary directory location. * * Defaults to `/psysh` inside the system's temp dir unless explicitly * overridden. * * @throws RuntimeException if no temporary directory is set and it is not possible to create one */ public function getRuntimeDir(): string { $runtimeDir = $this->configPaths->runtimeDir(); if (!\is_dir($runtimeDir)) { if (!@\mkdir($runtimeDir, 0700, true)) { throw new RuntimeException(\sprintf('Unable to create PsySH runtime directory. Make sure PHP is able to write to %s in order to continue.', \dirname($runtimeDir))); } } return $runtimeDir; } /** * Set the readline history file path. * * @param string $file */ public function setHistoryFile(string $file) { $this->historyFile = ConfigPaths::touchFileWithMkdir($file); } /** * Get the readline history file path. * * Defaults to `/history` inside the shell's base config dir unless * explicitly overridden. */ public function getHistoryFile(): string { if (isset($this->historyFile)) { return $this->historyFile; } $files = $this->configPaths->configFiles(['psysh_history', 'history']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple history files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } $this->setHistoryFile($files[0]); } else { // fallback: create our own history file $this->setHistoryFile($this->configPaths->currentConfigDir().'/psysh_history'); } return $this->historyFile; } /** * Set the readline max history size. * * @param int $value */ public function setHistorySize(int $value) { $this->historySize = (int) $value; } /** * Get the readline max history size. * * @return int */ public function getHistorySize() { return $this->historySize; } /** * Sets whether readline erases old duplicate history entries. * * @param bool $value */ public function setEraseDuplicates(bool $value) { $this->eraseDuplicates = (bool) $value; } /** * Get whether readline erases old duplicate history entries. * * @return bool|null */ public function getEraseDuplicates() { return $this->eraseDuplicates; } /** * Get a temporary file of type $type for process $pid. * * The file will be created inside the current temporary directory. * * @see self::getRuntimeDir * * @param string $type * @param int $pid * * @return string Temporary file name */ public function getTempFile(string $type, int $pid): string { return \tempnam($this->getRuntimeDir(), $type.'_'.$pid.'_'); } /** * Get a filename suitable for a FIFO pipe of $type for process $pid. * * The pipe will be created inside the current temporary directory. * * @param string $type * @param int $pid * * @return string Pipe name */ public function getPipe(string $type, int $pid): string { return \sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid); } /** * Check whether this PHP instance has Readline available. * * @return bool True if Readline is available */ public function hasReadline(): bool { return $this->hasReadline; } /** * Enable or disable Readline usage. * * @param bool $useReadline */ public function setUseReadline(bool $useReadline) { $this->useReadline = (bool) $useReadline; } /** * Check whether to use Readline. * * If `setUseReadline` as been set to true, but Readline is not actually * available, this will return false. * * @return bool True if the current Shell should use Readline */ public function useReadline(): bool { return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline; } /** * Set the Psy Shell readline service. * * @param Readline\Readline $readline */ public function setReadline(Readline\Readline $readline) { $this->readline = $readline; } /** * Get the Psy Shell readline service. * * By default, this service uses (in order of preference): * * * GNU Readline * * Libedit * * A transient array-based readline emulation. * * @return Readline\Readline */ public function getReadline(): Readline\Readline { if (!isset($this->readline)) { $className = $this->getReadlineClass(); $this->readline = new $className( $this->getHistoryFile(), $this->getHistorySize(), $this->getEraseDuplicates() ); } return $this->readline; } /** * Get the appropriate Readline implementation class name. * * @see self::getReadline */ private function getReadlineClass(): string { if ($this->useReadline()) { if (Readline\GNUReadline::isSupported()) { return Readline\GNUReadline::class; } elseif (Readline\Libedit::isSupported()) { return Readline\Libedit::class; } } if (Readline\Userland::isSupported()) { return Readline\Userland::class; } return Readline\Transient::class; } /** * Enable or disable bracketed paste. * * Note that this only works with readline (not libedit) integration for now. * * @param bool $useBracketedPaste */ public function setUseBracketedPaste(bool $useBracketedPaste) { $this->useBracketedPaste = (bool) $useBracketedPaste; } /** * Check whether to use bracketed paste with readline. * * When this works, it's magical. Tabs in pastes don't try to autcomplete. * Newlines in paste don't execute code until you get to the end. It makes * readline act like you'd expect when pasting. * * But it often (usually?) does not work. And when it doesn't, it just spews * escape codes all over the place and generally makes things ugly :( * * If `useBracketedPaste` has been set to true, but the current readline * implementation is anything besides GNU readline, this will return false. * * @return bool True if the shell should use bracketed paste */ public function useBracketedPaste(): bool { $readlineClass = $this->getReadlineClass(); return $this->useBracketedPaste && $readlineClass::supportsBracketedPaste(); // @todo mebbe turn this on by default some day? // return $readlineClass::supportsBracketedPaste() && $this->useBracketedPaste !== false; } /** * Check whether this PHP instance has Pcntl available. * * @return bool True if Pcntl is available */ public function hasPcntl(): bool { return $this->hasPcntl; } /** * Enable or disable Pcntl usage. * * @param bool $usePcntl */ public function setUsePcntl(bool $usePcntl) { $this->usePcntl = (bool) $usePcntl; } /** * Check whether to use Pcntl. * * If `setUsePcntl` has been set to true, but Pcntl is not actually * available, this will return false. * * @return bool True if the current Shell should use Pcntl */ public function usePcntl(): bool { if (!isset($this->usePcntl)) { // Unless pcntl is explicitly *enabled*, don't use it while XDebug is debugging. // See https://github.com/bobthecow/psysh/issues/742 if (\function_exists('xdebug_is_debugger_active') && \xdebug_is_debugger_active()) { return false; } return $this->hasPcntl; } return $this->hasPcntl && $this->usePcntl; } /** * Check whether to use raw output. * * This is set by the --raw-output (-r) flag, and really only makes sense * when non-interactive, e.g. executing stdin. * * @return bool true if raw output is enabled */ public function rawOutput(): bool { return $this->rawOutput; } /** * Enable or disable raw output. * * @param bool $rawOutput */ public function setRawOutput(bool $rawOutput) { $this->rawOutput = (bool) $rawOutput; } /** * Enable or disable strict requirement of semicolons. * * @see self::requireSemicolons() * * @param bool $requireSemicolons */ public function setRequireSemicolons(bool $requireSemicolons) { $this->requireSemicolons = (bool) $requireSemicolons; } /** * Check whether to require semicolons on all statements. * * By default, PsySH will automatically insert semicolons at the end of * statements if they're missing. To strictly require semicolons, set * `requireSemicolons` to true. */ public function requireSemicolons(): bool { return $this->requireSemicolons; } /** * Enable or disable Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. * * @param bool $useUnicode */ public function setUseUnicode(bool $useUnicode) { $this->useUnicode = (bool) $useUnicode; } /** * Check whether to use Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. */ public function useUnicode(): bool { if (isset($this->useUnicode)) { return $this->useUnicode; } // @todo detect `chsh` != 65001 on Windows and return false return true; } /** * Set the error logging level. * * @see self::errorLoggingLevel * * @param int $errorLoggingLevel */ public function setErrorLoggingLevel($errorLoggingLevel) { $this->errorLoggingLevel = (\E_ALL | \E_STRICT) & $errorLoggingLevel; } /** * Get the current error logging level. * * By default, PsySH will automatically log all errors, regardless of the * current `error_reporting` level. * * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it * to any valid error_reporting value to log only errors which match that * level. * * http://php.net/manual/en/function.error-reporting.php */ public function errorLoggingLevel(): int { return $this->errorLoggingLevel; } /** * Set a CodeCleaner service instance. * * @param CodeCleaner $cleaner */ public function setCodeCleaner(CodeCleaner $cleaner) { $this->cleaner = $cleaner; } /** * Get a CodeCleaner service instance. * * If none has been explicitly defined, this will create a new instance. */ public function getCodeCleaner(): CodeCleaner { if (!isset($this->cleaner)) { $this->cleaner = new CodeCleaner(null, null, null, $this->yolo()); } return $this->cleaner; } /** * Enable or disable running PsySH without input validation. * * You don't want this. */ public function setYolo($yolo) { $this->yolo = (bool) $yolo; } /** * Check whether to disable input validation. */ public function yolo(): bool { return $this->yolo; } /** * Enable or disable tab completion. * * @param bool $useTabCompletion */ public function setUseTabCompletion(bool $useTabCompletion) { $this->useTabCompletion = (bool) $useTabCompletion; } /** * @deprecated Call `setUseTabCompletion` instead * * @param bool $useTabCompletion */ public function setTabCompletion(bool $useTabCompletion) { $this->setUseTabCompletion($useTabCompletion); } /** * Check whether to use tab completion. * * If `setUseTabCompletion` has been set to true, but readline is not * actually available, this will return false. * * @return bool True if the current Shell should use tab completion */ public function useTabCompletion(): bool { return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline; } /** * @deprecated Call `useTabCompletion` instead */ public function getTabCompletion(): bool { return $this->useTabCompletion(); } /** * Set the Shell Output service. * * @param ShellOutput $output */ public function setOutput(ShellOutput $output) { $this->output = $output; $this->pipedOutput = null; // Reset cached pipe info if (isset($this->theme)) { $output->setTheme($this->theme); } $this->applyFormatterStyles(); } /** * Get a Shell Output service instance. * * If none has been explicitly provided, this will create a new instance * with the configured verbosity and output pager supplied by self::getPager * * @see self::verbosity * @see self::getPager */ public function getOutput(): ShellOutput { if (!isset($this->output)) { $this->setOutput(new ShellOutput( $this->getOutputVerbosity(), null, null, $this->getPager() ?: null, $this->theme() )); // This is racy because `getOutputDecorated` needs access to the // output stream to figure out if it's piped or not, so create it // first, then update after we have a stream. $decorated = $this->getOutputDecorated(); if ($decorated !== null) { $this->output->setDecorated($decorated); } } return $this->output; } /** * Get the decoration (i.e. color) setting for the Shell Output service. * * @return bool|null 3-state boolean corresponding to the current color mode */ public function getOutputDecorated() { switch ($this->colorMode()) { case self::COLOR_MODE_FORCED: return true; case self::COLOR_MODE_DISABLED: return false; case self::COLOR_MODE_AUTO: default: return $this->outputIsPiped() ? false : null; } } /** * Get the interactive setting for shell input. */ public function getInputInteractive(): bool { switch ($this->interactiveMode()) { case self::INTERACTIVE_MODE_FORCED: return true; case self::INTERACTIVE_MODE_DISABLED: return false; case self::INTERACTIVE_MODE_AUTO: default: return !$this->inputIsPiped(); } } /** * Set the OutputPager service. * * If a string is supplied, a ProcOutputPager will be used which shells out * to the specified command. * * `cat` is special-cased to use the PassthruPager directly. * * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance * * @param string|OutputPager|false $pager */ public function setPager($pager) { if ($pager === null || $pager === false || $pager === 'cat') { $pager = false; } if ($pager !== false && !\is_string($pager) && !$pager instanceof OutputPager) { throw new \InvalidArgumentException('Unexpected pager instance'); } $this->pager = $pager; } /** * Get an OutputPager instance or a command for an external Proc pager. * * If no Pager has been explicitly provided, and Pcntl is available, this * will default to `cli.pager` ini value, falling back to `which less`. * * @return string|OutputPager|false */ public function getPager() { if (!isset($this->pager) && $this->usePcntl()) { if (\getenv('TERM') === 'dumb') { return false; } if ($pager = \ini_get('cli.pager')) { // use the default pager $this->pager = $pager; } elseif ($less = $this->configPaths->which('less')) { // check for the presence of less... $this->pager = $less.' -R -F -X'; } } return $this->pager; } /** * Set the Shell AutoCompleter service. * * @param AutoCompleter $autoCompleter */ public function setAutoCompleter(AutoCompleter $autoCompleter) { $this->autoCompleter = $autoCompleter; } /** * Get an AutoCompleter service instance. */ public function getAutoCompleter(): AutoCompleter { if (!isset($this->autoCompleter)) { $this->autoCompleter = new AutoCompleter(); } return $this->autoCompleter; } /** * @deprecated Nothing should be using this anymore */ public function getTabCompletionMatchers(): array { return []; } /** * Add tab completion matchers to the AutoCompleter. * * This will buffer new matchers in the event that the Shell has not yet * been instantiated. This allows the user to specify matchers in their * config rc file, despite the fact that their file is needed in the Shell * constructor. * * @param array $matchers */ public function addMatchers(array $matchers) { $this->newMatchers = \array_merge($this->newMatchers, $matchers); if (isset($this->shell)) { $this->doAddMatchers(); } } /** * Internal method for adding tab completion matchers. This will set any new * matchers once a Shell is available. */ private function doAddMatchers() { if (!empty($this->newMatchers)) { $this->shell->addMatchers($this->newMatchers); $this->newMatchers = []; } } /** * @deprecated Use `addMatchers` instead * * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->addMatchers($matchers); } /** * Add commands to the Shell. * * This will buffer new commands in the event that the Shell has not yet * been instantiated. This allows the user to specify commands in their * config rc file, despite the fact that their file is needed in the Shell * constructor. * * @param array $commands */ public function addCommands(array $commands) { $this->newCommands = \array_merge($this->newCommands, $commands); if (isset($this->shell)) { $this->doAddCommands(); } } /** * Internal method for adding commands. This will set any new commands once * a Shell is available. */ private function doAddCommands() { if (!empty($this->newCommands)) { $this->shell->addCommands($this->newCommands); $this->newCommands = []; } } /** * Set the Shell backreference and add any new commands to the Shell. * * @param Shell $shell */ public function setShell(Shell $shell) { $this->shell = $shell; $this->doAddCommands(); $this->doAddMatchers(); } /** * Set the PHP manual database file. * * This file should be an SQLite database generated from the phpdoc source * with the `bin/build_manual` script. * * @param string $filename */ public function setManualDbFile(string $filename) { $this->manualDbFile = (string) $filename; } /** * Get the current PHP manual database file. * * @return string|null Default: '~/.local/share/psysh/php_manual.sqlite' */ public function getManualDbFile() { if (isset($this->manualDbFile)) { return $this->manualDbFile; } $files = $this->configPaths->dataFiles(['php_manual.sqlite']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple manual database files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } return $this->manualDbFile = $files[0]; } } /** * Get a PHP manual database connection. * * @return \PDO|null */ public function getManualDb() { if (!isset($this->manualDb)) { $dbFile = $this->getManualDbFile(); if ($dbFile !== null && \is_file($dbFile)) { try { $this->manualDb = new \PDO('sqlite:'.$dbFile); } catch (\PDOException $e) { if ($e->getMessage() === 'could not find driver') { throw new RuntimeException('SQLite PDO driver not found', 0, $e); } else { throw $e; } } } } return $this->manualDb; } /** * Add an array of casters definitions. * * @param array $casters */ public function addCasters(array $casters) { $this->getPresenter()->addCasters($casters); } /** * Get the Presenter service. */ public function getPresenter(): Presenter { if (!isset($this->presenter)) { $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes()); } return $this->presenter; } /** * Enable or disable warnings on multiple configuration or data files. * * @see self::warnOnMultipleConfigs() * * @param bool $warnOnMultipleConfigs */ public function setWarnOnMultipleConfigs(bool $warnOnMultipleConfigs) { $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs; } /** * Check whether to warn on multiple configuration or data files. * * By default, PsySH will use the file with highest precedence, and will * silently ignore all others. With this enabled, a warning will be emitted * (but not an exception thrown) if multiple configuration or data files * are found. * * This will default to true in a future release, but is false for now. */ public function warnOnMultipleConfigs(): bool { return $this->warnOnMultipleConfigs; } /** * Set the current color mode. * * @throws \InvalidArgumentException if the color mode isn't auto, forced or disabled * * @param string $colorMode */ public function setColorMode(string $colorMode) { $validColorModes = [ self::COLOR_MODE_AUTO, self::COLOR_MODE_FORCED, self::COLOR_MODE_DISABLED, ]; if (!\in_array($colorMode, $validColorModes)) { throw new \InvalidArgumentException('Invalid color mode: '.$colorMode); } $this->colorMode = $colorMode; } /** * Get the current color mode. */ public function colorMode(): string { return $this->colorMode; } /** * Set the shell's interactive mode. * * @throws \InvalidArgumentException if interactive mode isn't disabled, forced, or auto * * @param string $interactiveMode */ public function setInteractiveMode(string $interactiveMode) { $validInteractiveModes = [ self::INTERACTIVE_MODE_AUTO, self::INTERACTIVE_MODE_FORCED, self::INTERACTIVE_MODE_DISABLED, ]; if (!\in_array($interactiveMode, $validInteractiveModes)) { throw new \InvalidArgumentException('Invalid interactive mode: '.$interactiveMode); } $this->interactiveMode = $interactiveMode; } /** * Get the current interactive mode. */ public function interactiveMode(): string { return $this->interactiveMode; } /** * Set an update checker service instance. * * @param Checker $checker */ public function setChecker(Checker $checker) { $this->checker = $checker; } /** * Get an update checker service instance. * * If none has been explicitly defined, this will create a new instance. */ public function getChecker(): Checker { if (!isset($this->checker)) { $interval = $this->getUpdateCheck(); switch ($interval) { case Checker::ALWAYS: $this->checker = new GitHubChecker(); break; case Checker::DAILY: case Checker::WEEKLY: case Checker::MONTHLY: $checkFile = $this->getUpdateCheckCacheFile(); if ($checkFile === false) { $this->checker = new NoopChecker(); } else { $this->checker = new IntervalChecker($checkFile, $interval); } break; case Checker::NEVER: $this->checker = new NoopChecker(); break; } } return $this->checker; } /** * Get the current update check interval. * * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is * explicitly set, default to 'weekly'. */ public function getUpdateCheck(): string { return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY; } /** * Set the update check interval. * * @throws \InvalidArgumentException if the update check interval is unknown * * @param string $interval */ public function setUpdateCheck(string $interval) { $validIntervals = [ Checker::ALWAYS, Checker::DAILY, Checker::WEEKLY, Checker::MONTHLY, Checker::NEVER, ]; if (!\in_array($interval, $validIntervals)) { throw new \InvalidArgumentException('Invalid update check interval: '.$interval); } $this->updateCheck = $interval; } /** * Get a cache file path for the update checker. * * @return string|false Return false if config file/directory is not writable */ public function getUpdateCheckCacheFile() { return ConfigPaths::touchFileWithMkdir($this->configPaths->currentConfigDir().'/update_check.json'); } /** * Set the startup message. * * @param string $message */ public function setStartupMessage(string $message) { $this->startupMessage = $message; } /** * Get the startup message. * * @return string|null */ public function getStartupMessage() { return $this->startupMessage; } /** * Set the prompt. * * @deprecated The `prompt` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, prompt is applied first by the Theme, then overridden * by any explicitly defined prompt. * * Note that providing a prompt but not a theme config will implicitly use the `classic` theme. */ public function setPrompt(string $prompt) { $this->prompt = $prompt; if (isset($this->theme)) { $this->theme->setPrompt($prompt); } } /** * Get the prompt. * * @return string|null */ public function getPrompt() { return $this->prompt; } /** * Get the force array indexes. */ public function forceArrayIndexes(): bool { return $this->forceArrayIndexes; } /** * Set the force array indexes. * * @param bool $forceArrayIndexes */ public function setForceArrayIndexes(bool $forceArrayIndexes) { $this->forceArrayIndexes = $forceArrayIndexes; } /** * Set the current output Theme. * * @param Theme|string|array $theme Theme (or Theme config) */ public function setTheme($theme) { if (!$theme instanceof Theme) { $theme = new Theme($theme); } $this->theme = $theme; if (isset($this->prompt)) { $this->theme->setPrompt($this->prompt); } if (isset($this->output)) { $this->output->setTheme($theme); $this->applyFormatterStyles(); } } /** * Get the current output Theme. */ public function theme(): Theme { if (!isset($this->theme)) { // If a prompt is explicitly set, and a theme is not, base it on the `classic` theme. $this->theme = $this->prompt ? new Theme('classic') : new Theme(); } if (isset($this->prompt)) { $this->theme->setPrompt($this->prompt); } return $this->theme; } /** * Set the shell output formatter styles. * * Accepts a map from style name to [fg, bg, options], for example: * * [ * 'error' => ['white', 'red', ['bold']], * 'warning' => ['black', 'yellow'], * ] * * Foreground, background or options can be null, or even omitted entirely. * * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, styles are applied first by the Theme, then * overridden by any explicitly defined formatter styles. */ public function setFormatterStyles(array $formatterStyles) { foreach ($formatterStyles as $name => $style) { $this->formatterStyles[$name] = new OutputFormatterStyle(...$style); } if (isset($this->output)) { $this->applyFormatterStyles(); } } /** * Internal method for applying output formatter style customization. * * This is called on initialization of the shell output, and again if the * formatter styles config is updated. * * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, styles are applied first by the Theme, then * overridden by any explicitly defined formatter styles. */ private function applyFormatterStyles() { $formatter = $this->output->getFormatter(); foreach ($this->formatterStyles as $name => $style) { $formatter->setStyle($name, $style); } $errorFormatter = $this->output->getErrorOutput()->getFormatter(); foreach (Theme::ERROR_STYLES as $name) { if (isset($this->formatterStyles[$name])) { $errorFormatter->setStyle($name, $this->formatterStyles[$name]); } } } /** * Get the configured output verbosity. */ public function verbosity(): string { return $this->verbosity; } /** * Set the shell output verbosity. * * Accepts OutputInterface verbosity constants. * * @throws \InvalidArgumentException if verbosity level is invalid * * @param string $verbosity */ public function setVerbosity(string $verbosity) { $validVerbosityLevels = [ self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE, self::VERBOSITY_VERY_VERBOSE, self::VERBOSITY_DEBUG, ]; if (!\in_array($verbosity, $validVerbosityLevels)) { throw new \InvalidArgumentException('Invalid verbosity level: '.$verbosity); } $this->verbosity = $verbosity; if (isset($this->output)) { $this->output->setVerbosity($this->getOutputVerbosity()); } } /** * Map the verbosity configuration to OutputInterface verbosity constants. * * @return int OutputInterface verbosity level */ public function getOutputVerbosity(): int { switch ($this->verbosity()) { case self::VERBOSITY_QUIET: return OutputInterface::VERBOSITY_QUIET; case self::VERBOSITY_VERBOSE: return OutputInterface::VERBOSITY_VERBOSE; case self::VERBOSITY_VERY_VERBOSE: return OutputInterface::VERBOSITY_VERY_VERBOSE; case self::VERBOSITY_DEBUG: return OutputInterface::VERBOSITY_DEBUG; case self::VERBOSITY_NORMAL: default: return OutputInterface::VERBOSITY_NORMAL; } } /** * Guess whether stdin is piped. * * This is mostly useful for deciding whether to use non-interactive mode. */ public function inputIsPiped(): bool { if ($this->pipedInput === null) { $this->pipedInput = \defined('STDIN') && self::looksLikeAPipe(\STDIN); } return $this->pipedInput; } /** * Guess whether shell output is piped. * * This is mostly useful for deciding whether to use non-decorated output. */ public function outputIsPiped(): bool { if ($this->pipedOutput === null) { $this->pipedOutput = self::looksLikeAPipe($this->getOutput()->getStream()); } return $this->pipedOutput; } /** * Guess whether an input or output stream is piped. * * @param resource|int $stream */ private static function looksLikeAPipe($stream): bool { if (\function_exists('posix_isatty')) { return !\posix_isatty($stream); } $stat = \fstat($stream); $mode = $stat['mode'] & 0170000; return $mode === 0010000 || $mode === 0040000 || $mode === 0100000 || $mode === 0120000; } } VarDumper/PresenterAware.php000064400000001017150250565140012113 0ustar00 'number', 'integer' => 'integer', 'float' => 'float', 'const' => 'const', 'str' => 'string', 'cchr' => 'default', 'note' => 'class', 'ref' => 'default', 'public' => 'public', 'protected' => 'protected', 'private' => 'private', 'meta' => 'comment', 'key' => 'comment', 'index' => 'number', ]; public function __construct(OutputFormatter $formatter, $forceArrayIndexes = false) { // Work around https://github.com/symfony/symfony/issues/23572 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $this->dumper = new Dumper($formatter, $forceArrayIndexes); $this->dumper->setStyles($this->styles); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); $this->cloner = new Cloner(); $this->cloner->addCasters(['*' => function ($obj, array $a, Stub $stub, $isNested, $filter = 0) { if ($filter || $isNested) { if ($obj instanceof \Throwable) { $a = Caster::filter($a, Caster::EXCLUDE_NOT_IMPORTANT | Caster::EXCLUDE_EMPTY, $this->exceptionsImportants); } else { $a = Caster::filter($a, Caster::EXCLUDE_PROTECTED | Caster::EXCLUDE_PRIVATE); } } return $a; }]); } /** * Register casters. * * @see http://symfony.com/doc/current/components/var_dumper/advanced.html#casters * * @param callable[] $casters A map of casters */ public function addCasters(array $casters) { $this->cloner->addCasters($casters); } /** * Present a reference to the value. * * @param mixed $value */ public function presentRef($value): string { return $this->present($value, 0); } /** * Present a full representation of the value. * * If $depth is 0, the value will be presented as a ref instead. * * @param mixed $value * @param int $depth (default: null) * @param int $options One of Presenter constants */ public function present($value, int $depth = null, int $options = 0): string { $data = $this->cloner->cloneVar($value, !($options & self::VERBOSE) ? Caster::EXCLUDE_VERBOSE : 0); if (null !== $depth) { $data = $data->withMaxDepth($depth); } // Work around https://github.com/symfony/symfony/issues/23572 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $output = ''; $this->dumper->dump($data, function ($line, $depth) use (&$output) { if ($depth >= 0) { if ('' !== $output) { $output .= \PHP_EOL; } $output .= \str_repeat(' ', $depth).$line; } }); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); return OutputFormatter::escape($output); } } VarDumper/Dumper.php000064400000005673150250565140010434 0ustar00 '\0', "\t" => '\t', "\n" => '\n', "\v" => '\v', "\f" => '\f', "\r" => '\r', "\033" => '\e', ]; public function __construct(OutputFormatter $formatter, $forceArrayIndexes = false) { $this->formatter = $formatter; $this->forceArrayIndexes = $forceArrayIndexes; parent::__construct(); $this->setColors(false); } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, $type, $class, $hasChild) { if (Cursor::HASH_INDEXED === $type || Cursor::HASH_ASSOC === $type) { $class = 0; } parent::enterHash($cursor, $type, $class, $hasChild); } /** * {@inheritdoc} */ protected function dumpKey(Cursor $cursor) { if ($this->forceArrayIndexes || Cursor::HASH_INDEXED !== $cursor->hashType) { parent::dumpKey($cursor); } } protected function style($style, $value, $attr = []): string { if ('ref' === $style) { $value = \strtr($value, '@', '#'); } $styled = ''; $map = self::$controlCharsMap; $cchr = $this->styles['cchr']; $chunks = \preg_split(self::$controlCharsRx, $value, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); foreach ($chunks as $chunk) { if (\preg_match(self::$onlyControlCharsRx, $chunk)) { $chars = ''; $i = 0; do { $chars .= isset($map[$chunk[$i]]) ? $map[$chunk[$i]] : \sprintf('\x%02X', \ord($chunk[$i])); } while (isset($chunk[++$i])); $chars = $this->formatter->escape($chars); $styled .= "<{$cchr}>{$chars}"; } else { $styled .= $this->formatter->escape($chunk); } } $style = $this->styles[$style]; return "<{$style}>{$styled}"; } /** * {@inheritdoc} */ protected function dumpLine($depth, $endOfValue = false) { if ($endOfValue && 0 < $depth) { $this->line .= ','; } $this->line = $this->formatter->format($this->line); parent::dumpLine($depth, $endOfValue); } } VarDumper/Cloner.php000064400000001647150250565140010417 0ustar00filter = $filter; return parent::cloneVar($var, $filter); } /** * {@inheritdoc} */ protected function castResource(Stub $stub, $isNested): array { return Caster::EXCLUDE_VERBOSE & $this->filter ? [] : parent::castResource($stub, $isNested); } } functions.php000064400000012033150250565140007267 0ustar00 * @license http://opensource.org/licenses/MIT MIT * phpcs:disable Squiz.Functions.GlobalFunction */ declare(strict_types=1); namespace Ramsey\Uuid; use DateTimeInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; /** * Returns a version 1 (Gregorian time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|int|string|null $node A 48-bit number representing the * hardware address; this number may be represented as an integer or a * hexadecimal string * @param int|null $clockSeq A 14-bit number used to help avoid duplicates that * could arise when the clock is set backwards in time or if the node ID * changes * * @return non-empty-string Version 1 UUID as a string */ function v1($node = null, ?int $clockSeq = null): string { return Uuid::uuid1($node, $clockSeq)->toString(); } /** * Returns a version 2 (DCE Security) UUID from a local domain, local * identifier, host ID, clock sequence, and the current time * * @param int $localDomain The local domain to use when generating bytes, * according to DCE Security * @param IntegerObject|null $localIdentifier The local identifier for the * given domain; this may be a UID or GID on POSIX systems, if the local * domain is person or group, or it may be a site-defined identifier * if the local domain is org * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return non-empty-string Version 2 UUID as a string */ function v2( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): string { return Uuid::uuid2($localDomain, $localIdentifier, $node, $clockSeq)->toString(); } /** * Returns a version 3 (name-based) UUID based on the MD5 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * * @return non-empty-string Version 3 UUID as a string * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners */ function v3($ns, string $name): string { return Uuid::uuid3($ns, $name)->toString(); } /** * Returns a version 4 (random) UUID * * @return non-empty-string Version 4 UUID as a string */ function v4(): string { return Uuid::uuid4()->toString(); } /** * Returns a version 5 (name-based) UUID based on the SHA-1 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * * @return non-empty-string Version 5 UUID as a string * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners */ function v5($ns, string $name): string { return Uuid::uuid5($ns, $name)->toString(); } /** * Returns a version 6 (reordered time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates that * could arise when the clock is set backwards in time or if the node ID * changes * * @return non-empty-string Version 6 UUID as a string */ function v6(?Hexadecimal $node = null, ?int $clockSeq = null): string { return Uuid::uuid6($node, $clockSeq)->toString(); } /** * Returns a version 7 (Unix Epoch time) UUID * * @param DateTimeInterface|null $dateTime An optional date/time from which * to create the version 7 UUID. If not provided, the UUID is generated * using the current date/time. * * @return non-empty-string Version 7 UUID as a string */ function v7(?DateTimeInterface $dateTime = null): string { return Uuid::uuid7($dateTime)->toString(); } /** * Returns a version 8 (custom) UUID * * The bytes provided may contain any value according to your application's * needs. Be aware, however, that other applications may not understand the * semantics of the value. * * @param string $bytes A 16-byte octet string. This is an open blob * of data that you may fill with 128 bits of information. Be aware, * however, bits 48 through 51 will be replaced with the UUID version * field, and bits 64 and 65 will be replaced with the UUID variant. You * MUST NOT rely on these bits for your application needs. * * @return non-empty-string Version 8 UUID as a string */ function v8(string $bytes): string { return Uuid::uuid8($bytes)->toString(); } ConfigPaths.php000064400000026621150250565140007474 0ustar00overrideDirs($overrides); $this->env = $env ?: new SuperglobalsEnv(); } /** * Provide `configDir`, `dataDir` and `runtimeDir` overrides. * * If a key is set but empty, the override will be removed. If it is not set * at all, any existing override will persist. * * @param string[] $overrides Directory overrides */ public function overrideDirs(array $overrides) { if (\array_key_exists('configDir', $overrides)) { $this->configDir = $overrides['configDir'] ?: null; } if (\array_key_exists('dataDir', $overrides)) { $this->dataDir = $overrides['dataDir'] ?: null; } if (\array_key_exists('runtimeDir', $overrides)) { $this->runtimeDir = $overrides['runtimeDir'] ?: null; } } /** * Get the current home directory. * * @return string|null */ public function homeDir() { if ($homeDir = $this->getEnv('HOME') ?: $this->windowsHomeDir()) { return \strtr($homeDir, '\\', '/'); } return null; } private function windowsHomeDir() { if (\defined('PHP_WINDOWS_VERSION_MAJOR')) { $homeDrive = $this->getEnv('HOMEDRIVE'); $homePath = $this->getEnv('HOMEPATH'); if ($homeDrive && $homePath) { return $homeDrive.'/'.$homePath; } } return null; } private function homeConfigDir() { if ($homeConfigDir = $this->getEnv('XDG_CONFIG_HOME')) { return $homeConfigDir; } $homeDir = $this->homeDir(); return $homeDir === '/' ? $homeDir.'.config' : $homeDir.'/.config'; } /** * Get potential config directory paths. * * Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and all * XDG Base Directory config directories: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public function configDirs(): array { if ($this->configDir !== null) { return [$this->configDir]; } $configDirs = $this->getEnvArray('XDG_CONFIG_DIRS') ?: ['/etc/xdg']; return $this->allDirNames(\array_merge([$this->homeConfigDir()], $configDirs)); } /** * @deprecated */ public static function getConfigDirs(): array { return (new self())->configDirs(); } /** * Get potential home config directory paths. * * Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and the * XDG Base Directory home config directory: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @deprecated * * @return string[] */ public static function getHomeConfigDirs(): array { // Not quite the same, but this is deprecated anyway /shrug return self::getConfigDirs(); } /** * Get the current home config directory. * * Returns the highest precedence home config directory which actually * exists. If none of them exists, returns the highest precedence home * config directory (`%APPDATA%/PsySH` on Windows, `~/.config/psysh` * everywhere else). * * @see self::homeConfigDir */ public function currentConfigDir(): string { if ($this->configDir !== null) { return $this->configDir; } $configDirs = $this->allDirNames([$this->homeConfigDir()]); foreach ($configDirs as $configDir) { if (@\is_dir($configDir)) { return $configDir; } } return $configDirs[0]; } /** * @deprecated */ public static function getCurrentConfigDir(): string { return (new self())->currentConfigDir(); } /** * Find real config files in config directories. * * @param string[] $names Config file names * * @return string[] */ public function configFiles(array $names): array { return $this->allRealFiles($this->configDirs(), $names); } /** * @deprecated */ public static function getConfigFiles(array $names, $configDir = null): array { return (new self(['configDir' => $configDir]))->configFiles($names); } /** * Get potential data directory paths. * * If a `dataDir` option was explicitly set, returns an array containing * just that directory. * * Otherwise, it returns `~/.psysh` and all XDG Base Directory data directories: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public function dataDirs(): array { if ($this->dataDir !== null) { return [$this->dataDir]; } $homeDataDir = $this->getEnv('XDG_DATA_HOME') ?: $this->homeDir().'/.local/share'; $dataDirs = $this->getEnvArray('XDG_DATA_DIRS') ?: ['/usr/local/share', '/usr/share']; return $this->allDirNames(\array_merge([$homeDataDir], $dataDirs)); } /** * @deprecated */ public static function getDataDirs(): array { return (new self())->dataDirs(); } /** * Find real data files in config directories. * * @param string[] $names Config file names * * @return string[] */ public function dataFiles(array $names): array { return $this->allRealFiles($this->dataDirs(), $names); } /** * @deprecated */ public static function getDataFiles(array $names, $dataDir = null): array { return (new self(['dataDir' => $dataDir]))->dataFiles($names); } /** * Get a runtime directory. * * Defaults to `/psysh` inside the system's temp dir. */ public function runtimeDir(): string { if ($this->runtimeDir !== null) { return $this->runtimeDir; } // Fallback to a boring old folder in the system temp dir. $runtimeDir = $this->getEnv('XDG_RUNTIME_DIR') ?: \sys_get_temp_dir(); return \strtr($runtimeDir, '\\', '/').'/psysh'; } /** * @deprecated */ public static function getRuntimeDir(): string { return (new self())->runtimeDir(); } /** * Get a list of directories in PATH. * * If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'. * * @return string[] */ public function pathDirs(): array { return $this->getEnvArray('PATH') ?: ['/usr/sbin', '/usr/bin', '/sbin', '/bin']; } /** * Locate a command (an executable) in $PATH. * * Behaves like 'command -v COMMAND' or 'which COMMAND'. * If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'. * * @param string $command the executable to locate * * @return string */ public function which($command) { foreach ($this->pathDirs() as $path) { $fullpath = $path.\DIRECTORY_SEPARATOR.$command; if (@\is_file($fullpath) && @\is_executable($fullpath)) { return $fullpath; } } return null; } /** * Get all PsySH directory name candidates given a list of base directories. * * This expects that XDG-compatible directory paths will be passed in. * `psysh` will be added to each of $baseDirs, and we'll throw in `~/.psysh` * and a couple of Windows-friendly paths as well. * * @param string[] $baseDirs base directory paths * * @return string[] */ private function allDirNames(array $baseDirs): array { $dirs = \array_map(function ($dir) { return \strtr($dir, '\\', '/').'/psysh'; }, $baseDirs); // Add ~/.psysh if ($home = $this->getEnv('HOME')) { $dirs[] = \strtr($home, '\\', '/').'/.psysh'; } // Add some Windows specific ones :) if (\defined('PHP_WINDOWS_VERSION_MAJOR')) { if ($appData = $this->getEnv('APPDATA')) { // AppData gets preference \array_unshift($dirs, \strtr($appData, '\\', '/').'/PsySH'); } if ($windowsHomeDir = $this->windowsHomeDir()) { $dir = \strtr($windowsHomeDir, '\\', '/').'/.psysh'; if (!\in_array($dir, $dirs)) { $dirs[] = $dir; } } } return $dirs; } /** * Given a list of directories, and a list of filenames, find the ones that * are real files. * * @return string[] */ private function allRealFiles(array $dirNames, array $fileNames): array { $files = []; foreach ($dirNames as $dir) { foreach ($fileNames as $name) { $file = $dir.'/'.$name; if (@\is_file($file)) { $files[] = $file; } } } return $files; } /** * Ensure that $dir exists and is writable. * * Generates E_USER_NOTICE error if the directory is not writable or creatable. * * @param string $dir * * @return bool False if directory exists but is not writeable, or cannot be created */ public static function ensureDir(string $dir): bool { if (!\is_dir($dir)) { // Just try making it and see if it works @\mkdir($dir, 0700, true); } if (!\is_dir($dir) || !\is_writable($dir)) { \trigger_error(\sprintf('Writing to directory %s is not allowed.', $dir), \E_USER_NOTICE); return false; } return true; } /** * Ensure that $file exists and is writable, make the parent directory if necessary. * * Generates E_USER_NOTICE error if either $file or its directory is not writable. * * @param string $file * * @return string|false Full path to $file, or false if file is not writable */ public static function touchFileWithMkdir(string $file) { if (\file_exists($file)) { if (\is_writable($file)) { return $file; } \trigger_error(\sprintf('Writing to %s is not allowed.', $file), \E_USER_NOTICE); return false; } if (!self::ensureDir(\dirname($file))) { return false; } \touch($file); return $file; } private function getEnv($key) { return $this->env->get($key); } private function getEnvArray($key) { if ($value = $this->getEnv($key)) { return \explode(\PATH_SEPARATOR, $value); } return null; } } Readline/Userland.php000064400000007203150250565140010562 0ustar00hoaReadline = new HoaReadline(); $this->hoaReadline->addMapping('\C-l', function () { $this->redisplay(); return HoaReadline::STATE_NO_ECHO; }); $this->tput = new HoaConsoleTput(); HoaConsole::setTput($this->tput); $this->input = new HoaConsoleInput(); HoaConsole::setInput($this->input); $this->output = new HoaConsoleOutput(); HoaConsole::setOutput($this->output); } /** * Bootstrap some things that Hoa used to do itself. */ public static function bootstrapHoa(bool $withTerminalResize = false) { // A side effect registers hoa:// stream wrapper \class_exists('Psy\Readline\Hoa\ProtocolWrapper'); // A side effect registers hoa://Library/Stream \class_exists('Psy\Readline\Hoa\Stream'); // A side effect binds terminal resize $withTerminalResize && \class_exists('Psy\Readline\Hoa\ConsoleWindow'); } /** * {@inheritdoc} */ public function addHistory(string $line): bool { $this->hoaReadline->addHistory($line); return true; } /** * {@inheritdoc} */ public function clearHistory(): bool { $this->hoaReadline->clearHistory(); return true; } /** * {@inheritdoc} */ public function listHistory(): array { $i = 0; $list = []; while (($item = $this->hoaReadline->getHistory($i++)) !== null) { $list[] = $item; } return $list; } /** * {@inheritdoc} */ public function readHistory(): bool { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return string */ public function readline(string $prompt = null) { $this->lastPrompt = $prompt; return $this->hoaReadline->readLine($prompt); } /** * {@inheritdoc} */ public function redisplay() { $currentLine = $this->hoaReadline->getLine(); HoaConsoleCursor::clear('all'); echo $this->lastPrompt, $currentLine; } /** * {@inheritdoc} */ public function writeHistory(): bool { return true; } } Readline/GNUReadline.php000064400000010044150250565140011077 0ustar00historyFile = ($historyFile !== null) ? $historyFile : false; $this->historySize = $historySize; $this->eraseDups = $eraseDups; \readline_info('readline_name', 'psysh'); } /** * {@inheritdoc} */ public function addHistory(string $line): bool { if ($res = \readline_add_history($line)) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function clearHistory(): bool { if ($res = \readline_clear_history()) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function listHistory(): array { return \readline_list_history(); } /** * {@inheritdoc} */ public function readHistory(): bool { \readline_read_history(); \readline_clear_history(); return \readline_read_history($this->historyFile); } /** * {@inheritdoc} */ public function readline(string $prompt = null) { return \readline($prompt); } /** * {@inheritdoc} */ public function redisplay() { \readline_redisplay(); } /** * {@inheritdoc} */ public function writeHistory(): bool { // We have to write history first, since it is used // by Libedit to list history if ($this->historyFile !== false) { $res = \readline_write_history($this->historyFile); } else { $res = true; } if (!$res || !$this->eraseDups && !$this->historySize > 0) { return $res; } $hist = $this->listHistory(); if (!$hist) { return true; } if ($this->eraseDups) { // flip-flip technique: removes duplicates, latest entries win. $hist = \array_flip(\array_flip($hist)); // sort on keys to get the order back \ksort($hist); } if ($this->historySize > 0) { $histsize = \count($hist); if ($histsize > $this->historySize) { $hist = \array_slice($hist, $histsize - $this->historySize); } } \readline_clear_history(); foreach ($hist as $line) { \readline_add_history($line); } if ($this->historyFile !== false) { return \readline_write_history($this->historyFile); } return true; } } Readline/Transient.php000064400000005677150250565140010771 0ustar00history = []; $this->historySize = $historySize; $this->eraseDups = $eraseDups; } /** * {@inheritdoc} */ public function addHistory(string $line): bool { if ($this->eraseDups) { if (($key = \array_search($line, $this->history)) !== false) { unset($this->history[$key]); } } $this->history[] = $line; if ($this->historySize > 0) { $histsize = \count($this->history); if ($histsize > $this->historySize) { $this->history = \array_slice($this->history, $histsize - $this->historySize); } } $this->history = \array_values($this->history); return true; } /** * {@inheritdoc} */ public function clearHistory(): bool { $this->history = []; return true; } /** * {@inheritdoc} */ public function listHistory(): array { return $this->history; } /** * {@inheritdoc} */ public function readHistory(): bool { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return false|string */ public function readline(string $prompt = null) { echo $prompt; return \rtrim(\fgets($this->getStdin()), "\n\r"); } /** * {@inheritdoc} */ public function redisplay() { // noop } /** * {@inheritdoc} */ public function writeHistory(): bool { return true; } /** * Get a STDIN file handle. * * @throws BreakException if user hits Ctrl+D * * @return resource */ private function getStdin() { if (!isset($this->stdin)) { $this->stdin = \fopen('php://stdin', 'r'); } if (\feof($this->stdin)) { throw new BreakException('Ctrl+D'); } return $this->stdin; } } Readline/Readline.php000064400000003447150250565140010536 0ustar00_reach) as $part) { $out[] = "\r".$part.\strtolower($head).$queue; } $out[] = "\r".\dirname(__DIR__, 5).$queue; return \implode(';', $out); } $out = []; foreach (\explode(';', $this->_reach) as $part) { $pos = \strrpos(\rtrim($part, \DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR) + 1; $head = \substr($part, 0, $pos); $tail = \substr($part, $pos); $out[] = $head.\strtolower($tail); } $this->_reach = \implode(';', $out); return parent::reach($queue); } } Readline/Hoa/IteratorFileSystem.php000064400000005372150250565140013317 0ustar00_splFileInfoClass = $splFileInfoClass; if (null === $flags) { parent::__construct($path); } else { parent::__construct($path, $flags); } return; } /** * Current. * Please, see \FileSystemIterator::current() method. */ #[\ReturnTypeWillChange] public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); } return $out; } } Readline/Hoa/Console.php000064400000017461150250565140011125 0ustar00setAutocompleters($autocompleters); return; } /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. */ public function complete(&$prefix) { foreach ($this->getAutocompleters() as $autocompleter) { $preg = \preg_match( '#('.$autocompleter->getWordDefinition().')$#u', $prefix, $match ); if (0 === $preg) { continue; } $_prefix = $match[0]; if (null === $out = $autocompleter->complete($_prefix)) { continue; } $prefix = $_prefix; return $out; } return null; } /** * Set/initialize list of autocompleters. */ protected function setAutocompleters(array $autocompleters) { $old = $this->_autocompleters; $this->_autocompleters = new \ArrayObject($autocompleters); return $old; } /** * Get list of autocompleters. */ public function getAutocompleters() { return $this->_autocompleters; } /** * Get definition of a word. */ public function getWordDefinition(): string { return '.*'; } } Readline/Hoa/FileGeneric.php000064400000026261150250565140011675 0ustar00getStreamName()); } /** * Get directory name component of path. */ public function getDirname(): string { return \dirname($this->getStreamName()); } /** * Get size. */ public function getSize(): int { if (false === $this->getStatistic()) { return false; } return \filesize($this->getStreamName()); } /** * Get informations about a file. */ public function getStatistic(): array { return \fstat($this->getStream()); } /** * Get last access time of file. */ public function getATime(): int { return \fileatime($this->getStreamName()); } /** * Get inode change time of file. */ public function getCTime(): int { return \filectime($this->getStreamName()); } /** * Get file modification time. */ public function getMTime(): int { return \filemtime($this->getStreamName()); } /** * Get file group. */ public function getGroup(): int { return \filegroup($this->getStreamName()); } /** * Get file owner. */ public function getOwner(): int { return \fileowner($this->getStreamName()); } /** * Get file permissions. */ public function getPermissions(): int { return \fileperms($this->getStreamName()); } /** * Get file permissions as a string. * Result sould be interpreted like this: * * s: socket; * * l: symbolic link; * * -: regular; * * b: block special; * * d: directory; * * c: character special; * * p: FIFO pipe; * * u: unknown. */ public function getReadablePermissions(): string { $p = $this->getPermissions(); if (($p & 0xC000) === 0xC000) { $out = 's'; } elseif (($p & 0xA000) === 0xA000) { $out = 'l'; } elseif (($p & 0x8000) === 0x8000) { $out = '-'; } elseif (($p & 0x6000) === 0x6000) { $out = 'b'; } elseif (($p & 0x4000) === 0x4000) { $out = 'd'; } elseif (($p & 0x2000) === 0x2000) { $out = 'c'; } elseif (($p & 0x1000) === 0x1000) { $out = 'p'; } else { $out = 'u'; } $out .= (($p & 0x0100) ? 'r' : '-'). (($p & 0x0080) ? 'w' : '-'). (($p & 0x0040) ? (($p & 0x0800) ? 's' : 'x') : (($p & 0x0800) ? 'S' : '-')). (($p & 0x0020) ? 'r' : '-'). (($p & 0x0010) ? 'w' : '-'). (($p & 0x0008) ? (($p & 0x0400) ? 's' : 'x') : (($p & 0x0400) ? 'S' : '-')). (($p & 0x0004) ? 'r' : '-'). (($p & 0x0002) ? 'w' : '-'). (($p & 0x0001) ? (($p & 0x0200) ? 't' : 'x') : (($p & 0x0200) ? 'T' : '-')); return $out; } /** * Check if the file is readable. */ public function isReadable(): bool { return \is_readable($this->getStreamName()); } /** * Check if the file is writable. */ public function isWritable(): bool { return \is_writable($this->getStreamName()); } /** * Check if the file is executable. */ public function isExecutable(): bool { return \is_executable($this->getStreamName()); } /** * Clear file status cache. */ public function clearStatisticCache() { \clearstatcache(true, $this->getStreamName()); } /** * Clear all files status cache. */ public static function clearAllStatisticCaches() { \clearstatcache(); } /** * Set access and modification time of file. */ public function touch(int $time = null, int $atime = null): bool { if (null === $time) { $time = \time(); } if (null === $atime) { $atime = $time; } return \touch($this->getStreamName(), $time, $atime); } /** * Copy file. * Return the destination file path if succeed, false otherwise. */ public function copy(string $to, bool $force = StreamTouchable::DO_NOT_OVERWRITE): bool { $from = $this->getStreamName(); if ($force === StreamTouchable::DO_NOT_OVERWRITE && true === \file_exists($to)) { return true; } if (null === $this->getStreamContext()) { return @\copy($from, $to); } return @\copy($from, $to, $this->getStreamContext()->getContext()); } /** * Move a file. */ public function move( string $name, bool $force = StreamTouchable::DO_NOT_OVERWRITE, bool $mkdir = StreamTouchable::DO_NOT_MAKE_DIRECTORY ): bool { $from = $this->getStreamName(); if ($force === StreamTouchable::DO_NOT_OVERWRITE && true === \file_exists($name)) { return false; } if (StreamTouchable::MAKE_DIRECTORY === $mkdir) { FileDirectory::create( \dirname($name), FileDirectory::MODE_CREATE_RECURSIVE ); } if (null === $this->getStreamContext()) { return @\rename($from, $name); } return @\rename($from, $name, $this->getStreamContext()->getContext()); } /** * Delete a file. */ public function delete(): bool { if (null === $this->getStreamContext()) { return @\unlink($this->getStreamName()); } return @\unlink( $this->getStreamName(), $this->getStreamContext()->getContext() ); } /** * Change file group. */ public function changeGroup($group): bool { return \chgrp($this->getStreamName(), $group); } /** * Change file mode. */ public function changeMode(int $mode): bool { return \chmod($this->getStreamName(), $mode); } /** * Change file owner. */ public function changeOwner($user): bool { return \chown($this->getStreamName(), $user); } /** * Change the current umask. */ public static function umask(int $umask = null): int { if (null === $umask) { return \umask(); } return \umask($umask); } /** * Check if it is a file. */ public function isFile(): bool { return \is_file($this->getStreamName()); } /** * Check if it is a link. */ public function isLink(): bool { return \is_link($this->getStreamName()); } /** * Check if it is a directory. */ public function isDirectory(): bool { return \is_dir($this->getStreamName()); } /** * Check if it is a socket. */ public function isSocket(): bool { return \filetype($this->getStreamName()) === 'socket'; } /** * Check if it is a FIFO pipe. */ public function isFIFOPipe(): bool { return \filetype($this->getStreamName()) === 'fifo'; } /** * Check if it is character special file. */ public function isCharacterSpecial(): bool { return \filetype($this->getStreamName()) === 'char'; } /** * Check if it is block special. */ public function isBlockSpecial(): bool { return \filetype($this->getStreamName()) === 'block'; } /** * Check if it is an unknown type. */ public function isUnknown(): bool { return \filetype($this->getStreamName()) === 'unknown'; } /** * Set the open mode. */ protected function setMode(string $mode) { $old = $this->_mode; $this->_mode = $mode; return $old; } /** * Get the open mode. */ public function getMode() { return $this->_mode; } /** * Get inode. */ public function getINode(): int { return \fileinode($this->getStreamName()); } /** * Check if the system is case sensitive or not. */ public static function isCaseSensitive(): bool { return !( \file_exists(\mb_strtolower(__FILE__)) && \file_exists(\mb_strtoupper(__FILE__)) ); } /** * Get a canonicalized absolute pathname. */ public function getRealPath(): string { if (false === $out = \realpath($this->getStreamName())) { return $this->getStreamName(); } return $out; } /** * Get file extension (if exists). */ public function getExtension(): string { return \pathinfo( $this->getStreamName(), \PATHINFO_EXTENSION ); } /** * Get filename without extension. */ public function getFilename(): string { $file = \basename($this->getStreamName()); if (\defined('PATHINFO_FILENAME')) { return \pathinfo($file, \PATHINFO_FILENAME); } if (\strstr($file, '.')) { return \substr($file, 0, \strrpos($file, '.')); } return $file; } } Readline/Hoa/StreamBufferable.php000064400000004674150250565140012736 0ustar00_open()` method. Please, see the `self::_getStream()` method. */ public function __construct(string $streamName, string $context = null, bool $wait = false) { $this->_streamName = $streamName; $this->_context = $context; $this->_hasBeenDeferred = $wait; $this->setListener( new EventListener( $this, [ 'authrequire', 'authresult', 'complete', 'connect', 'failure', 'mimetype', 'progress', 'redirect', 'resolve', 'size', ] ) ); if (true === $wait) { return; } $this->open(); return; } /** * Get a stream in the register. * If the stream does not exist, try to open it by calling the * $handler->_open() method. */ private static function &_getStream( string $streamName, self $handler, string $context = null ): array { $name = \md5($streamName); if (null !== $context) { if (false === StreamContext::contextExists($context)) { throw new StreamException('Context %s was not previously declared, cannot retrieve '.'this context.', 0, $context); } $context = StreamContext::getInstance($context); } if (!isset(self::$_register[$name])) { self::$_register[$name] = [ self::NAME => $streamName, self::HANDLER => $handler, self::RESOURCE => $handler->_open($streamName, $context), self::CONTEXT => $context, ]; Event::register( 'hoa://Event/Stream/'.$streamName, $handler ); // Add :open-ready? Event::register( 'hoa://Event/Stream/'.$streamName.':close-before', $handler ); } else { $handler->_borrowing = true; } if (null === self::$_register[$name][self::RESOURCE]) { self::$_register[$name][self::RESOURCE] = $handler->_open($streamName, $context); } return self::$_register[$name]; } /** * Open the stream and return the associated resource. * Note: This method is protected, but do not forget that it could be * overloaded into a public context. */ abstract protected function &_open(string $streamName, StreamContext $context = null); /** * Close the current stream. * Note: this method is protected, but do not forget that it could be * overloaded into a public context. */ abstract protected function _close(): bool; /** * Open the stream. */ final public function open(): self { $context = $this->_context; if (true === $this->hasBeenDeferred()) { if (null === $context) { $handle = StreamContext::getInstance(\uniqid()); $handle->setParameters([ 'notification' => [$this, '_notify'], ]); $context = $handle->getId(); } elseif (true === StreamContext::contextExists($context)) { $handle = StreamContext::getInstance($context); $parameters = $handle->getParameters(); if (!isset($parameters['notification'])) { $handle->setParameters([ 'notification' => [$this, '_notify'], ]); } } } $this->_bufferSize = self::DEFAULT_BUFFER_SIZE; $this->_bucket = self::_getStream( $this->_streamName, $this, $context ); return $this; } /** * Close the current stream. */ final public function close() { $streamName = $this->getStreamName(); if (null === $streamName) { return; } $name = \md5($streamName); if (!isset(self::$_register[$name])) { return; } Event::notify( 'hoa://Event/Stream/'.$streamName.':close-before', $this, new EventBucket() ); if (false === $this->_close()) { return; } unset(self::$_register[$name]); $this->_bucket[self::HANDLER] = null; Event::unregister( 'hoa://Event/Stream/'.$streamName ); Event::unregister( 'hoa://Event/Stream/'.$streamName.':close-before' ); return; } /** * Get the current stream name. */ public function getStreamName() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::NAME]; } /** * Get the current stream. */ public function getStream() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::RESOURCE]; } /** * Get the current stream context. */ public function getStreamContext() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::CONTEXT]; } /** * Get stream handler according to its name. */ public static function getStreamHandler(string $streamName) { $name = \md5($streamName); if (!isset(self::$_register[$name])) { return null; } return self::$_register[$name][self::HANDLER]; } /** * Set the current stream. Useful to manage a stack of streams (e.g. socket * and select). Notice that it could be unsafe to use this method without * taking time to think about it two minutes. Resource of type “Unknown” is * considered as valid. */ public function _setStream($stream) { if (false === \is_resource($stream) && ('resource' !== \gettype($stream) || 'Unknown' !== \get_resource_type($stream))) { throw new StreamException('Try to change the stream resource with an invalid one; '.'given %s.', 1, \gettype($stream)); } $old = $this->_bucket[self::RESOURCE]; $this->_bucket[self::RESOURCE] = $stream; return $old; } /** * Check if the stream is opened. */ public function isOpened(): bool { return \is_resource($this->getStream()); } /** * Set the timeout period. */ public function setStreamTimeout(int $seconds, int $microseconds = 0): bool { return \stream_set_timeout($this->getStream(), $seconds, $microseconds); } /** * Whether the opening of the stream has been deferred. */ protected function hasBeenDeferred() { return $this->_hasBeenDeferred; } /** * Check whether the connection has timed out or not. * This is basically a shortcut of `getStreamMetaData` + the `timed_out` * index, but the resulting code is more readable. */ public function hasTimedOut(): bool { $metaData = $this->getStreamMetaData(); return true === $metaData['timed_out']; } /** * Set blocking/non-blocking mode. */ public function setStreamBlocking(bool $mode): bool { return \stream_set_blocking($this->getStream(), $mode); } /** * Set stream buffer. * Output using fwrite() (or similar function) is normally buffered at 8 Ko. * This means that if there are two processes wanting to write to the same * output stream, each is paused after 8 Ko of data to allow the other to * write. */ public function setStreamBuffer(int $buffer): bool { // Zero means success. $out = 0 === \stream_set_write_buffer($this->getStream(), $buffer); if (true === $out) { $this->_bufferSize = $buffer; } return $out; } /** * Disable stream buffering. * Alias of $this->setBuffer(0). */ public function disableStreamBuffer(): bool { return $this->setStreamBuffer(0); } /** * Get stream buffer size. */ public function getStreamBufferSize(): int { return $this->_bufferSize; } /** * Get stream wrapper name. */ public function getStreamWrapperName(): string { if (false === $pos = \strpos($this->getStreamName(), '://')) { return 'file'; } return \substr($this->getStreamName(), 0, $pos); } /** * Get stream meta data. */ public function getStreamMetaData(): array { return \stream_get_meta_data($this->getStream()); } /** * Whether this stream is already opened by another handler. */ public function isBorrowing(): bool { return $this->_borrowing; } /** * Notification callback. */ public function _notify( int $ncode, int $severity, $message, $code, $transferred, $max ) { static $_map = [ \STREAM_NOTIFY_AUTH_REQUIRED => 'authrequire', \STREAM_NOTIFY_AUTH_RESULT => 'authresult', \STREAM_NOTIFY_COMPLETED => 'complete', \STREAM_NOTIFY_CONNECT => 'connect', \STREAM_NOTIFY_FAILURE => 'failure', \STREAM_NOTIFY_MIME_TYPE_IS => 'mimetype', \STREAM_NOTIFY_PROGRESS => 'progress', \STREAM_NOTIFY_REDIRECTED => 'redirect', \STREAM_NOTIFY_RESOLVE => 'resolve', \STREAM_NOTIFY_FILE_SIZE_IS => 'size', ]; $this->getListener()->fire($_map[$ncode], new EventBucket([ 'code' => $code, 'severity' => $severity, 'message' => $message, 'transferred' => $transferred, 'max' => $max, ])); } /** * Call the $handler->close() method on each stream in the static stream * register. * This method does not check the return value of $handler->close(). Thus, * if a stream is persistent, the $handler->close() should do anything. It * is a very generic method. */ final public static function _Hoa_Stream() { foreach (self::$_register as $entry) { $entry[self::HANDLER]->close(); } return; } /** * Transform object to string. */ public function __toString(): string { return $this->getStreamName(); } /** * Close the stream when destructing. */ public function __destruct() { if (false === $this->isOpened()) { return; } $this->close(); return; } } /** * Class \Hoa\Stream\_Protocol. * * The `hoa://Library/Stream` node. * * @license New BSD License */ class _Protocol extends ProtocolNode { /** * Component's name. * * @var string */ protected $_name = 'Stream'; /** * ID of the component. * * @param string $id ID of the component * * @return mixed */ public function reachId(string $id) { return Stream::getStreamHandler($id); } } /* * Shutdown method. */ \register_shutdown_function([Stream::class, '_Hoa_Stream']); /** * Add the `hoa://Library/Stream` node. Should be use to reach/get an entry * in the stream register. */ $protocol = Protocol::getInstance(); $protocol['Library'][] = new _Protocol(); Readline/Hoa/StreamIn.php000064400000005471150250565140011243 0ustar00read(). */ public function readString(int $length); /** * Read a character. * It could be equivalent to $this->read(1). */ public function readCharacter(); /** * Read a boolean. */ public function readBoolean(); /** * Read an integer. */ public function readInteger(int $length = 1); /** * Read a float. */ public function readFloat(int $length = 1); /** * Read an array. * In most cases, it could be an alias to the $this->scanf() method. */ public function readArray(); /** * Read a line. */ public function readLine(); /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0); /** * Parse input from a stream according to a format. */ public function scanf(string $format): array; } Readline/Hoa/EventSource.php000064400000003363150250565140011761 0ustar00_flags = IteratorFileSystem::KEY_AS_PATHNAME | IteratorFileSystem::CURRENT_AS_FILEINFO | IteratorFileSystem::SKIP_DOTS; $this->_first = \RecursiveIteratorIterator::SELF_FIRST; return; } /** * Select a directory to scan. */ public function in($paths): self { if (!\is_array($paths)) { $paths = [$paths]; } foreach ($paths as $path) { if (1 === \preg_match('/[\*\?\[\]]/', $path)) { $iterator = new \CallbackFilterIterator( new \GlobIterator(\rtrim($path, \DIRECTORY_SEPARATOR)), function ($current) { return $current->isDir(); } ); foreach ($iterator as $fileInfo) { $this->_paths[] = $fileInfo->getPathname(); } } else { $this->_paths[] = $path; } } return $this; } /** * Set max depth for recursion. */ public function maxDepth(int $depth): self { $this->_maxDepth = $depth; return $this; } /** * Include files in the result. */ public function files(): self { $this->_types[] = 'file'; return $this; } /** * Include directories in the result. */ public function directories(): self { $this->_types[] = 'dir'; return $this; } /** * Include links in the result. */ public function links(): self { $this->_types[] = 'link'; return $this; } /** * Follow symbolink links. */ public function followSymlinks(bool $flag = true): self { if (true === $flag) { $this->_flags ^= IteratorFileSystem::FOLLOW_SYMLINKS; } else { $this->_flags |= IteratorFileSystem::FOLLOW_SYMLINKS; } return $this; } /** * Include files that match a regex. * Example: * $this->name('#\.php$#');. */ public function name(string $regex): self { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { return 0 !== \preg_match($regex, $current->getBasename()); }; return $this; } /** * Exclude directories that match a regex. * Example: * $this->notIn('#^\.(git|hg)$#');. */ public function notIn(string $regex): self { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { foreach (\explode(\DIRECTORY_SEPARATOR, $current->getPathname()) as $part) { if (0 !== \preg_match($regex, $part)) { return false; } } return true; }; return $this; } /** * Include files that respect a certain size. * The size is a string of the form: * operator number unit * where * • operator could be: <, <=, >, >= or =; * • number is a positive integer; * • unit could be: b (default), Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb. * Example: * $this->size('>= 12Kb');. */ public function size(string $size): self { if (0 === \preg_match('#^(<|<=|>|>=|=)\s*(\d+)\s*((?:[KMGTPEZY])b)?$#', $size, $matches)) { return $this; } $number = (float) ($matches[2]); $unit = $matches[3] ?? 'b'; $operator = $matches[1]; switch ($unit) { case 'b': break; // kilo case 'Kb': $number <<= 10; break; // mega. case 'Mb': $number <<= 20; break; // giga. case 'Gb': $number <<= 30; break; // tera. case 'Tb': $number *= 1099511627776; break; // peta. case 'Pb': $number *= 1024 ** 5; break; // exa. case 'Eb': $number *= 1024 ** 6; break; // zetta. case 'Zb': $number *= 1024 ** 7; break; // yota. case 'Yb': $number *= 1024 ** 8; break; } $filter = null; switch ($operator) { case '<': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() < $number; }; break; case '<=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() <= $number; }; break; case '>': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() > $number; }; break; case '>=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() >= $number; }; break; case '=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() === $number; }; break; } $this->_filters[] = $filter; return $this; } /** * Whether we should include dots or not (respectively . and ..). */ public function dots(bool $flag = true): self { if (true === $flag) { $this->_flags ^= IteratorFileSystem::SKIP_DOTS; } else { $this->_flags |= IteratorFileSystem::SKIP_DOTS; } return $this; } /** * Include files that are owned by a certain owner. */ public function owner(int $owner): self { $this->_filters[] = function (\SplFileInfo $current) use ($owner) { return $current->getOwner() === $owner; }; return $this; } /** * Format date. * Date can have the following syntax: * date * since date * until date * If the date does not have the “ago” keyword, it will be added. * Example: “42 hours” is equivalent to “since 42 hours” which is equivalent * to “since 42 hours ago”. */ protected function formatDate(string $date, &$operator): int { $operator = -1; if (0 === \preg_match('#\bago\b#', $date)) { $date .= ' ago'; } if (0 !== \preg_match('#^(since|until)\b(.+)$#', $date, $matches)) { $time = \strtotime($matches[2]); if ('until' === $matches[1]) { $operator = 1; } } else { $time = \strtotime($date); } return $time; } /** * Include files that have been changed from a certain date. * Example: * $this->changed('since 13 days');. */ public function changed(string $date): self { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() < $time; }; } return $this; } /** * Include files that have been modified from a certain date. * Example: * $this->modified('since 13 days');. */ public function modified(string $date): self { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() < $time; }; } return $this; } /** * Add your own filter. * The callback will receive 3 arguments: $current, $key and $iterator. It * must return a boolean: true to include the file, false to exclude it. * Example: * // Include files that are readable * $this->filter(function ($current) { * return $current->isReadable(); * });. */ public function filter($callback): self { $this->_filters[] = $callback; return $this; } /** * Sort result by name. * If \Collator exists (from ext/intl), the $locale argument will be used * for its constructor. Else, strcmp() will be used. * Example: * $this->sortByName('fr_FR');. */ public function sortByName(string $locale = 'root'): self { if (true === \class_exists('Collator', false)) { $collator = new \Collator($locale); $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) use ($collator) { return $collator->compare($a->getPathname(), $b->getPathname()); }; } else { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return \strcmp($a->getPathname(), $b->getPathname()); }; } return $this; } /** * Sort result by size. * Example: * $this->sortBySize();. */ public function sortBySize(): self { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return $a->getSize() < $b->getSize(); }; return $this; } /** * Add your own sort. * The callback will receive 2 arguments: $a and $b. Please see the uasort() * function. * Example: * // Sort files by their modified time. * $this->sort(function ($a, $b) { * return $a->getMTime() < $b->getMTime(); * });. */ public function sort($callable): self { $this->_sorts[] = $callable; return $this; } /** * Child comes first when iterating. */ public function childFirst(): self { $this->_first = \RecursiveIteratorIterator::CHILD_FIRST; return $this; } /** * Get the iterator. */ public function getIterator() { $_iterator = new \AppendIterator(); $types = $this->getTypes(); if (!empty($types)) { $this->_filters[] = function (\SplFileInfo $current) use ($types) { return \in_array($current->getType(), $types); }; } $maxDepth = $this->getMaxDepth(); $splFileInfo = $this->getSplFileInfo(); foreach ($this->getPaths() as $path) { if (1 === $maxDepth) { $iterator = new \IteratorIterator( new IteratorRecursiveDirectory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); } else { $iterator = new \RecursiveIteratorIterator( new IteratorRecursiveDirectory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); if (1 < $maxDepth) { $iterator->setMaxDepth($maxDepth - 1); } } $_iterator->append($iterator); } foreach ($this->getFilters() as $filter) { $_iterator = new \CallbackFilterIterator( $_iterator, $filter ); } $sorts = $this->getSorts(); if (empty($sorts)) { return $_iterator; } $array = \iterator_to_array($_iterator); foreach ($sorts as $sort) { \uasort($array, $sort); } return new \ArrayIterator($array); } /** * Set SplFileInfo classname. */ public function setSplFileInfo(string $splFileInfo): string { $old = $this->_splFileInfo; $this->_splFileInfo = $splFileInfo; return $old; } /** * Get SplFileInfo classname. */ public function getSplFileInfo(): string { return $this->_splFileInfo; } /** * Get all paths. */ protected function getPaths(): array { return $this->_paths; } /** * Get max depth. */ public function getMaxDepth(): int { return $this->_maxDepth; } /** * Get types. */ public function getTypes(): array { return $this->_types; } /** * Get filters. */ protected function getFilters(): array { return $this->_filters; } /** * Get sorts. */ protected function getSorts(): array { return $this->_sorts; } /** * Get flags. */ public function getFlags(): int { return $this->_flags; } /** * Get first. */ public function getFirst(): int { return $this->_first; } } Readline/Hoa/ProtocolWrapper.php000064400000032602150250565140012657 0ustar00resolve($path, $exists); } /** * Retrieve the underlying resource. * * `$castAs` can be `STREAM_CAST_FOR_SELECT` when `stream_select` is * calling `stream_cast` or `STREAM_CAST_AS_STREAM` when `stream_cast` is * called for other uses. */ public function stream_cast(int $castAs) { return null; } /** * Closes a resource. * This method is called in response to `fclose`. * All resources that were locked, or allocated, by the wrapper should be * released. */ public function stream_close() { if (true === @\fclose($this->getStream())) { $this->_stream = null; $this->_streamName = null; } } /** * Tests for end-of-file on a file pointer. * This method is called in response to feof(). */ public function stream_eof(): bool { return \feof($this->getStream()); } /** * Flush the output. * This method is called in respond to fflush(). * If we have cached data in our stream but not yet stored it into the * underlying storage, we should do so now. */ public function stream_flush(): bool { return \fflush($this->getStream()); } /** * Advisory file locking. * This method is called in response to flock(), when file_put_contents() * (when flags contains LOCK_EX), stream_set_blocking() and when closing the * stream (LOCK_UN). * * Operation is one the following: * * LOCK_SH to acquire a shared lock (reader) ; * * LOCK_EX to acquire an exclusive lock (writer) ; * * LOCK_UN to release a lock (shared or exclusive) ; * * LOCK_NB if we don't want flock() to * block while locking (not supported on * Windows). */ public function stream_lock(int $operation): bool { return \flock($this->getStream(), $operation); } /** * Change stream options. * This method is called to set metadata on the stream. It is called when * one of the following functions is called on a stream URL: touch, chmod, * chown or chgrp. * * Option must be one of the following constant: * * STREAM_META_TOUCH, * * STREAM_META_OWNER_NAME, * * STREAM_META_OWNER, * * STREAM_META_GROUP_NAME, * * STREAM_META_GROUP, * * STREAM_META_ACCESS. * * Values are arguments of `touch`, `chmod`, `chown`, and `chgrp`. */ public function stream_metadata(string $path, int $option, $values): bool { $path = static::realPath($path, false); switch ($option) { case \STREAM_META_TOUCH: $arity = \count($values); if (0 === $arity) { $out = \touch($path); } elseif (1 === $arity) { $out = \touch($path, $values[0]); } else { $out = \touch($path, $values[0], $values[1]); } break; case \STREAM_META_OWNER_NAME: case \STREAM_META_OWNER: $out = \chown($path, $values); break; case \STREAM_META_GROUP_NAME: case \STREAM_META_GROUP: $out = \chgrp($path, $values); break; case \STREAM_META_ACCESS: $out = \chmod($path, $values); break; default: $out = false; } return $out; } /** * Open file or URL. * This method is called immediately after the wrapper is initialized (f.e. * by fopen() and file_get_contents()). */ public function stream_open(string $path, string $mode, int $options, &$openedPath): bool { $path = static::realPath($path, 'r' === $mode[0]); if (Protocol::NO_RESOLUTION === $path) { return false; } if (null === $this->context) { $openedPath = \fopen($path, $mode, $options & \STREAM_USE_PATH); } else { $openedPath = \fopen( $path, $mode, (bool) ($options & \STREAM_USE_PATH), $this->context ); } if (false === \is_resource($openedPath)) { return false; } $this->_stream = $openedPath; $this->_streamName = $path; return true; } /** * Read from stream. * This method is called in response to fread() and fgets(). */ public function stream_read(int $size): string { return \fread($this->getStream(), $size); } /** * Seek to specific location in a stream. * This method is called in response to fseek(). * The read/write position of the stream should be updated according to the * $offset and $whence. * * The possible values for `$whence` are: * * SEEK_SET to set position equal to $offset bytes, * * SEEK_CUR to set position to current location plus `$offset`, * * SEEK_END to set position to end-of-file plus `$offset`. */ public function stream_seek(int $offset, int $whence = \SEEK_SET): bool { return 0 === \fseek($this->getStream(), $offset, $whence); } /** * Retrieve information about a file resource. * This method is called in response to fstat(). */ public function stream_stat(): array { return \fstat($this->getStream()); } /** * Retrieve the current position of a stream. * This method is called in response to ftell(). */ public function stream_tell(): int { return \ftell($this->getStream()); } /** * Truncate a stream to a given length. */ public function stream_truncate(int $size): bool { return \ftruncate($this->getStream(), $size); } /** * Write to stream. * This method is called in response to fwrite(). */ public function stream_write(string $data): int { return \fwrite($this->getStream(), $data); } /** * Close directory handle. * This method is called in to closedir(). * Any resources which were locked, or allocated, during opening and use of * the directory stream should be released. */ public function dir_closedir() { \closedir($this->getStream()); $this->_stream = null; $this->_streamName = null; } /** * Open directory handle. * This method is called in response to opendir(). * * The `$options` input represents whether or not to enforce safe_mode * (0x04). It is not used here. */ public function dir_opendir(string $path, int $options): bool { $path = static::realPath($path); $handle = null; if (null === $this->context) { $handle = @\opendir($path); } else { $handle = @\opendir($path, $this->context); } if (false === $handle) { return false; } $this->_stream = $handle; $this->_streamName = $path; return true; } /** * Read entry from directory handle. * This method is called in response to readdir(). * * @return mixed */ public function dir_readdir() { return \readdir($this->getStream()); } /** * Rewind directory handle. * This method is called in response to rewinddir(). * Should reset the output generated by self::dir_readdir, i.e. the next * call to self::dir_readdir should return the first entry in the location * returned by self::dir_opendir. */ public function dir_rewinddir() { \rewinddir($this->getStream()); } /** * Create a directory. * This method is called in response to mkdir(). */ public function mkdir(string $path, int $mode, int $options): bool { if (null === $this->context) { return \mkdir( static::realPath($path, false), $mode, $options | \STREAM_MKDIR_RECURSIVE ); } return \mkdir( static::realPath($path, false), $mode, (bool) ($options | \STREAM_MKDIR_RECURSIVE), $this->context ); } /** * Rename a file or directory. * This method is called in response to rename(). * Should attempt to rename $from to $to. */ public function rename(string $from, string $to): bool { if (null === $this->context) { return \rename(static::realPath($from), static::realPath($to, false)); } return \rename( static::realPath($from), static::realPath($to, false), $this->context ); } /** * Remove a directory. * This method is called in response to rmdir(). * The `$options` input is a bitwise mask of values. It is not used here. */ public function rmdir(string $path, int $options): bool { if (null === $this->context) { return \rmdir(static::realPath($path)); } return \rmdir(static::realPath($path), $this->context); } /** * Delete a file. * This method is called in response to unlink(). */ public function unlink(string $path): bool { if (null === $this->context) { return \unlink(static::realPath($path)); } return \unlink(static::realPath($path), $this->context); } /** * Retrieve information about a file. * This method is called in response to all stat() related functions. * The `$flags` input holds additional flags set by the streams API. It * can hold one or more of the following values OR'd together. * STREAM_URL_STAT_LINK: for resource with the ability to link to other * resource (such as an HTTP location: forward, or a filesystem * symlink). This flag specified that only information about the link * itself should be returned, not the resource pointed to by the * link. This flag is set in response to calls to lstat(), is_link(), or * filetype(). STREAM_URL_STAT_QUIET: if this flag is set, our wrapper * should not raise any errors. If this flag is not set, we are * responsible for reporting errors using the trigger_error() function * during stating of the path. */ public function url_stat(string $path, int $flags) { $path = static::realPath($path); if (Protocol::NO_RESOLUTION === $path) { if ($flags & \STREAM_URL_STAT_QUIET) { return 0; } else { return \trigger_error( 'Path '.$path.' cannot be resolved.', \E_WARNING ); } } if ($flags & \STREAM_URL_STAT_LINK) { return @\lstat($path); } return @\stat($path); } /** * Get stream resource. */ public function getStream() { return $this->_stream; } /** * Get stream name. */ public function getStreamName() { return $this->_streamName; } } /* * Register the `hoa://` protocol. */ \stream_wrapper_register('hoa', ProtocolWrapper::class); Readline/Hoa/ConsoleWindow.php000064400000030231150250565140012303 0ustar00writeAll("\033[8;".$y.';'.$x.'t'); return; } /** * Get current size (x and y) of the window. */ public static function getSize(): array { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { $modecon = \explode("\n", \ltrim(ConsoleProcessus::execute('mode con'))); $_y = \trim($modecon[2]); \preg_match('#[^:]+:\s*([0-9]+)#', $_y, $matches); $y = (int) $matches[1]; $_x = \trim($modecon[3]); \preg_match('#[^:]+:\s*([0-9]+)#', $_x, $matches); $x = (int) $matches[1]; return [ 'x' => $x, 'y' => $y, ]; } $term = ''; if (isset($_SERVER['TERM'])) { $term = 'TERM="'.$_SERVER['TERM'].'" '; } $command = $term.'tput cols && '.$term.'tput lines'; $tput = ConsoleProcessus::execute($command, false); if (!empty($tput)) { list($x, $y) = \explode("\n", $tput); return [ 'x' => (int) $x, 'y' => (int) $y, ]; } // DECSLPP. Console::getOutput()->writeAll("\033[18t"); $input = Console::getInput(); // Read \033[8;y;xt. $input->read(4); // skip \033, [, 8 and ;. $x = null; $y = null; $handle = &$y; while (true) { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$x; break; case 't': break 2; default: if (false === \ctype_digit($char)) { break 2; } $handle .= $char; } } if (null === $x || null === $y) { return [ 'x' => 0, 'y' => 0, ]; } return [ 'x' => (int) $x, 'y' => (int) $y, ]; } /** * Move to X and Y (in pixels). */ public static function moveTo(int $x, int $y) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[3;".$x.';'.$y.'t'); return; } /** * Get current position (x and y) of the window (in pixels). */ public static function getPosition(): array { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return ['x' => 0, 'y' => 0]; } // DECSLPP. Console::getOutput()->writeAll("\033[13t"); $input = Console::getInput(); // Read \033[3;x;yt. $input->read(4); // skip \033, [, 3 and ;. $x = null; $y = null; $handle = &$x; while (true) { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$y; break; case 't': break 2; default: $handle .= $char; } } return [ 'x' => (int) $x, 'y' => (int) $y, ]; } /** * Scroll whole page. * Directions can be: * • u, up, ↑ : scroll whole page up; * • d, down, ↓ : scroll whole page down. * Directions can be concatenated by a single space. */ public static function scroll(string $directions, int $repeat = 1) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } if (1 > $repeat) { return; } elseif (1 === $repeat) { $handle = \explode(' ', $directions); } else { $handle = \explode(' ', $directions, 1); } $tput = Console::getTput(); $count = ['up' => 0, 'down' => 0]; foreach ($handle as $direction) { switch ($direction) { case 'u': case 'up': case '↑': ++$count['up']; break; case 'd': case 'down': case '↓': ++$count['down']; break; } } $output = Console::getOutput(); if (0 < $count['up']) { $output->writeAll( \str_replace( '%p1%d', $count['up'] * $repeat, $tput->get('parm_index') ) ); } if (0 < $count['down']) { $output->writeAll( \str_replace( '%p1%d', $count['down'] * $repeat, $tput->get('parm_rindex') ) ); } return; } /** * Minimize the window. */ public static function minimize() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[2t"); return; } /** * Restore the window (de-minimize). */ public static function restore() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[1t"); return; } /** * Raise the window to the front of the stacking order. */ public static function raise() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[5t"); return; } /** * Lower the window to the bottom of the stacking order. */ public static function lower() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[6t"); return; } /** * Set title. */ public static function setTitle(string $title) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033]0;".$title."\033\\"); return; } /** * Get title. */ public static function getTitle() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return null; } // DECSLPP. Console::getOutput()->writeAll("\033[21t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === \stream_select($read, $write, $except, 0, 50000)) { return $out; } // Read \033]l\033\ $input->read(3); // skip \033, ] and l. while (true) { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } return $out; } /** * Get label. */ public static function getLabel() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return null; } // DECSLPP. Console::getOutput()->writeAll("\033[20t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === \stream_select($read, $write, $except, 0, 50000)) { return $out; } // Read \033]L<label>\033\ $input->read(3); // skip \033, ] and L. while (true) { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } return $out; } /** * Refresh the window. */ public static function refresh() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[7t"); return; } /** * Set clipboard value. */ public static function copy(string $data) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } $out = "\033]52;;".\base64_encode($data)."\033\\"; $output = Console::getOutput(); $considerMultiplexer = $output->considerMultiplexer(true); $output->writeAll($out); $output->considerMultiplexer($considerMultiplexer); return; } } /* * Advanced interaction. */ Console::advancedInteraction(); /* * Event. */ if (\function_exists('pcntl_signal')) { ConsoleWindow::getInstance(); \pcntl_signal( \SIGWINCH, function () { static $_window = null; if (null === $_window) { $_window = ConsoleWindow::getInstance(); } Event::notify( 'hoa://Event/Console/Window:resize', $_window, new EventBucket([ 'size' => ConsoleWindow::getSize(), ]) ); } ); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileException.php����������������������������������������������������������������������0000644�����������������00000003465�15025056514�0012260 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Exception. * * Extending the \Hoa\Exception\Exception class. * * @license New BSD License */ class FileException extends Exception { } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileLinkReadWrite.php������������������������������������������������������������������0000644�����������������00000015476�15025056514�0013033 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Link\ReadWrite. * * File handler. */ class FileLinkReadWrite extends FileLink implements StreamIn, StreamOut { /** * Open a file. */ public function __construct( string $streamName, string $mode = parent::MODE_APPEND_READ_WRITE, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ_WRITE, parent::MODE_TRUNCATE_READ_WRITE, parent::MODE_APPEND_READ_WRITE, parent::MODE_CREATE_READ_WRITE, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName) && parent::MODE_READ_WRITE === $this->getMode()) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->read($length); } /** * Read a character. */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } /** * Write n characters. */ public function write(string $string, int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 3, $length); } return \fwrite($this->getStream(), $string, $length); } /** * Write a string. */ public function writeString(string $string) { $string = (string) $string; return $this->write($string, \strlen($string)); } /** * Write a character. */ public function writeCharacter(string $char) { return $this->write((string) $char[0], 1); } /** * Write a boolean. */ public function writeBoolean(bool $boolean) { return $this->write((string) (bool) $boolean, 1); } /** * Write an integer. */ public function writeInteger(int $integer) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer)); } /** * Write a float. */ public function writeFloat(float $float) { $float = (string) (float) $float; return $this->write($float, \strlen($float)); } /** * Write an array. */ public function writeArray(array $array) { $array = \var_export($array, true); return $this->write($array, \strlen($array)); } /** * Write a line. */ public function writeLine(string $line) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1); } ++$n; return $this->write(\substr($line, 0, $n), $n); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string) { return $this->write($string, \strlen($string)); } /** * Truncate a file to a given length. */ public function truncate(int $size): bool { return \ftruncate($this->getStream(), $size); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamLockable.php���������������������������������������������������������������������0000644�����������������00000005060�15025056514�0012403 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Lockable. * * Interface for lockable input/output. * * @license New BSD License */ interface StreamLockable extends IStream { /** * Acquire a shared lock (reader). * * @const int */ const LOCK_SHARED = \LOCK_SH; /** * Acquire an exclusive lock (writer). * * @const int */ const LOCK_EXCLUSIVE = \LOCK_EX; /** * Release a lock (shared or exclusive). * * @const int */ const LOCK_RELEASE = \LOCK_UN; /** * If we do not want $this->lock() to block while locking. * * @const int */ const LOCK_NO_BLOCK = \LOCK_NB; /** * Portable advisory locking. * Should take a look at stream_supports_lock(). * * @param int $operation operation, use the self::LOCK_* constants * * @return bool */ public function lock(int $operation): bool; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamContext.php����������������������������������������������������������������������0000644�����������������00000007055�15025056514�0012321 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Stream\Context. * * Make a multiton of stream contexts. */ class StreamContext { /** * Context ID. */ protected $_id = null; /** * @var resource */ protected $_context; /** * Multiton. */ protected static $_instances = []; /** * Construct a context. */ protected function __construct($id) { $this->_id = $id; $this->_context = \stream_context_create(); return; } /** * Multiton. */ public static function getInstance(string $id): self { if (false === static::contextExists($id)) { static::$_instances[$id] = new self($id); } return static::$_instances[$id]; } /** * Get context ID. */ public function getId(): string { return $this->_id; } /** * Check if a context exists. */ public static function contextExists(string $id): bool { return \array_key_exists($id, static::$_instances); } /** * Set options. * Please, see http://php.net/context. */ public function setOptions(array $options): bool { return \stream_context_set_option($this->getContext(), $options); } /** * Set parameters. * Please, see http://php.net/context.params. */ public function setParameters(array $parameters): bool { return \stream_context_set_params($this->getContext(), $parameters); } /** * Get options. */ public function getOptions(): array { return \stream_context_get_options($this->getContext()); } /** * Get parameters. */ public function getParameters(): array { return \stream_context_get_params($this->getContext()); } /** * Get context as a resource. */ public function getContext() { return $this->_context; } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/ProtocolNode.php�����������������������������������������������������������������������0000644�����������������00000017657�15025056514�0012141 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Abstract class for all `hoa://`'s nodes. */ class ProtocolNode implements \ArrayAccess, \IteratorAggregate { /** * Node's name. */ protected $_name = null; /** * Path for the `reach` method. */ protected $_reach = null; /** * Children of the node. */ private $_children = []; /** * Construct a protocol's node. * If it is not a data object (i.e. if it does not extend this class to * overload the `$_name` attribute), we can set the `$_name` attribute * dynamically. This is useful to create a node on-the-fly. */ public function __construct(string $name = null, string $reach = null, array $children = []) { if (null !== $name) { $this->_name = $name; } if (null !== $reach) { $this->_reach = $reach; } foreach ($children as $child) { $this[] = $child; } return; } /** * Add a node. */ #[\ReturnTypeWillChange] public function offsetSet($name, $node) { if (!($node instanceof self)) { throw new ProtocolException('Protocol node must extend %s.', 0, __CLASS__); } if (empty($name)) { $name = $node->getName(); } if (empty($name)) { throw new ProtocolException('Cannot add a node to the `hoa://` protocol without a name.', 1); } $this->_children[$name] = $node; } /** * Get a specific node. */ public function offsetGet($name): self { if (!isset($this[$name])) { throw new ProtocolException('Node %s does not exist.', 2, $name); } return $this->_children[$name]; } /** * Check if a node exists. */ public function offsetExists($name): bool { return true === \array_key_exists($name, $this->_children); } /** * Remove a node. */ #[\ReturnTypeWillChange] public function offsetUnset($name) { unset($this->_children[$name]); } /** * Resolve a path, i.e. iterate the nodes tree and reach the queue of * the path. */ protected function _resolve(string $path, &$accumulator, string $id = null) { if (\substr($path, 0, 6) === 'hoa://') { $path = \substr($path, 6); } if (empty($path)) { return null; } if (null === $accumulator) { $accumulator = []; $posId = \strpos($path, '#'); if (false !== $posId) { $id = \substr($path, $posId + 1); $path = \substr($path, 0, $posId); } else { $id = null; } } $path = \trim($path, '/'); $pos = \strpos($path, '/'); if (false !== $pos) { $next = \substr($path, 0, $pos); } else { $next = $path; } if (isset($this[$next])) { if (false === $pos) { if (null === $id) { $this->_resolveChoice($this[$next]->reach(), $accumulator); return true; } $accumulator = null; return $this[$next]->reachId($id); } $tnext = $this[$next]; $this->_resolveChoice($tnext->reach(), $accumulator); return $tnext->_resolve(\substr($path, $pos + 1), $accumulator, $id); } $this->_resolveChoice($this->reach($path), $accumulator); return true; } /** * Resolve choices, i.e. a reach value has a “;”. */ protected function _resolveChoice($reach, &$accumulator) { if (null === $reach) { $reach = ''; } if (empty($accumulator)) { $accumulator = \explode(';', $reach); return; } if (false === \strpos($reach, ';')) { if (false !== $pos = \strrpos($reach, "\r")) { $reach = \substr($reach, $pos + 1); foreach ($accumulator as &$entry) { $entry = null; } } foreach ($accumulator as &$entry) { $entry .= $reach; } return; } $choices = \explode(';', $reach); $ref = $accumulator; $accumulator = []; foreach ($choices as $choice) { if (false !== $pos = \strrpos($choice, "\r")) { $choice = \substr($choice, $pos + 1); foreach ($ref as $entry) { $accumulator[] = $choice; } } else { foreach ($ref as $entry) { $accumulator[] = $entry.$choice; } } } unset($ref); return; } /** * Queue of the node. * Generic one. Must be overrided in children classes. */ public function reach(string $queue = null) { return empty($queue) ? $this->_reach : $queue; } /** * ID of the component. * Generic one. Should be overrided in children classes. */ public function reachId(string $id) { throw new ProtocolException('The node %s has no ID support (tried to reach #%s).', 4, [$this->getName(), $id]); } /** * Set a new reach value. */ public function setReach(string $reach) { $old = $this->_reach; $this->_reach = $reach; return $old; } /** * Get node's name. */ public function getName() { return $this->_name; } /** * Get reach's root. */ protected function getReach() { return $this->_reach; } /** * Get an iterator. */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->_children); } /** * Get root the protocol. */ public static function getRoot(): Protocol { return Protocol::getInstance(); } /** * Print a tree of component. */ public function __toString(): string { static $i = 0; $out = \str_repeat(' ', $i).$this->getName()."\n"; foreach ($this as $node) { ++$i; $out .= $node; --$i; } return $out; } } ���������������������������������������������������������������������������������Readline/Hoa/StreamOut.php��������������������������������������������������������������������������0000644�����������������00000005250�15025056514�0011437 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Out. * * Interface for output. */ interface StreamOut extends IStream { /** * Write n characters. */ public function write(string $string, int $length); /** * Write a string. */ public function writeString(string $string); /** * Write a character. */ public function writeCharacter(string $character); /** * Write a boolean. */ public function writeBoolean(bool $boolean); /** * Write an integer. */ public function writeInteger(int $integer); /** * Write a float. */ public function writeFloat(float $float); /** * Write an array. */ public function writeArray(array $array); /** * Write a line. */ public function writeLine(string $line); /** * Write all, i.e. as much as possible. */ public function writeAll(string $string); /** * Truncate a stream to a given length. */ public function truncate(int $size): bool; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamException.php��������������������������������������������������������������������0000644�����������������00000003427�15025056514�0012632 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Stream\Exception. * * Extending the \Hoa\Exception\Exception class. */ class StreamException extends Exception { } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileRead.php���������������������������������������������������������������������������0000644�����������������00000011116�15025056514�0011165 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Read. * * File handler. */ class FileRead extends File implements StreamIn { /** * Open a file. */ public function __construct( string $streamName, string $mode = parent::MODE_READ, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName)) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->read($length); } /** * Read a character. */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/ProtocolException.php������������������������������������������������������������������0000644�����������������00000003365�15025056514�0013201 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Extends the `Hoa\Exception\Exception` class. */ class ProtocolException extends Exception { } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamPointable.php��������������������������������������������������������������������0000644�����������������00000004620�15025056514�0012605 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Pointable. * * Interface for pointable input/output. */ interface StreamPointable extends IStream { /** * Set position equal to $offset bytes. */ const SEEK_SET = \SEEK_SET; /** * Set position to current location plus $offset. */ const SEEK_CURRENT = \SEEK_CUR; /** * Set position to end-of-file plus $offset. */ const SEEK_END = \SEEK_END; /** * Rewind the position of a stream pointer. */ public function rewind(): bool; /** * Seek on a stream pointer. */ public function seek(int $offset, int $whence = self::SEEK_SET): int; /** * Get the current position of the stream pointer. */ public function tell(): int; } ����������������������������������������������������������������������������������������������������������������Readline/Hoa/FileLinkRead.php�����������������������������������������������������������������������0000644�����������������00000013541�15025056514�0012007 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Link\Read. * * File handler. * * @license New BSD License */ class FileLinkRead extends FileLink implements StreamIn { /** * Open a file. * * @param string $streamName stream name * @param string $mode open mode, see the parent::MODE_* constants * @param string $context context ID (please, see the * \Hoa\Stream\Context class) * @param bool $wait differ opening or not */ public function __construct( string $streamName, string $mode = parent::MODE_READ, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. * * @param string $streamName Stream name (e.g. path or URL). * @param \Hoa\Stream\Context $context context * * @return resource * * @throws \Hoa\File\Exception\FileDoesNotExist * @throws \Hoa\File\Exception */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName)) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. * * @return bool */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. * * @param int $length length * * @return string * * @throws \Hoa\File\Exception */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). * * @param int $length length * * @return string */ public function readString(int $length) { return $this->read($length); } /** * Read a character. * * @return string */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. * * @return bool */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. * * @param int $length length * * @return int */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. * * @param int $length length * * @return float */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. * * @param string $format format (see printf's formats) * * @return array */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. * * @return string */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. * * @param int $offset offset * * @return string */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. * * @param string $format format (see printf's formats) * * @return array */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Readline.php���������������������������������������������������������������������������0000644�����������������00000064567�15025056514�0011257 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Readline. * * Read, edit, bind… a line from the input. */ class Readline { /** * State: continue to read. */ const STATE_CONTINUE = 1; /** * State: stop to read. */ const STATE_BREAK = 2; /** * State: no output the current buffer. */ const STATE_NO_ECHO = 4; /** * Current editing line. */ protected $_line = null; /** * Current editing line seek. */ protected $_lineCurrent = 0; /** * Current editing line length. */ protected $_lineLength = 0; /** * Current buffer (most of the time, a char). */ protected $_buffer = null; /** * Mapping. */ protected $_mapping = []; /** * History. */ protected $_history = []; /** * History current position. */ protected $_historyCurrent = 0; /** * History size. */ protected $_historySize = 0; /** * Prefix. */ protected $_prefix = null; /** * Autocompleter. */ protected $_autocompleter = null; /** * Initialize the readline editor. */ public function __construct() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } $this->_mapping["\033[A"] = [$this, '_bindArrowUp']; $this->_mapping["\033[B"] = [$this, '_bindArrowDown']; $this->_mapping["\033[C"] = [$this, '_bindArrowRight']; $this->_mapping["\033[D"] = [$this, '_bindArrowLeft']; $this->_mapping["\001"] = [$this, '_bindControlA']; $this->_mapping["\002"] = [$this, '_bindControlB']; $this->_mapping["\005"] = [$this, '_bindControlE']; $this->_mapping["\006"] = [$this, '_bindControlF']; $this->_mapping["\010"] = $this->_mapping["\177"] = [$this, '_bindBackspace']; $this->_mapping["\027"] = [$this, '_bindControlW']; $this->_mapping["\n"] = [$this, '_bindNewline']; $this->_mapping["\t"] = [$this, '_bindTab']; return; } /** * Read a line from the input. */ public function readLine(string $prefix = null) { $input = Console::getInput(); if (true === $input->eof()) { return false; } $direct = Console::isDirect($input->getStream()->getStream()); $output = Console::getOutput(); if (false === $direct || \defined('PHP_WINDOWS_VERSION_PLATFORM')) { $out = $input->readLine(); if (false === $out) { return false; } $out = \substr($out, 0, -1); if (true === $direct) { $output->writeAll($prefix); } else { $output->writeAll($prefix.$out."\n"); } return $out; } $this->resetLine(); $this->setPrefix($prefix); $read = [$input->getStream()->getStream()]; $write = $except = []; $output->writeAll($prefix); while (true) { @\stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } $char = $this->_read(); $this->_buffer = $char; $return = $this->_readLine($char); if (0 === ($return & self::STATE_NO_ECHO)) { $output->writeAll($this->_buffer); } if (0 !== ($return & self::STATE_BREAK)) { break; } } return $this->getLine(); } /** * Readline core. */ public function _readLine(string $char) { if (isset($this->_mapping[$char]) && \is_callable($this->_mapping[$char])) { $mapping = $this->_mapping[$char]; return $mapping($this); } if (isset($this->_mapping[$char])) { $this->_buffer = $this->_mapping[$char]; } elseif (false === Ustring::isCharPrintable($char)) { ConsoleCursor::bip(); return static::STATE_CONTINUE | static::STATE_NO_ECHO; } if ($this->getLineLength() === $this->getLineCurrent()) { $this->appendLine($this->_buffer); return static::STATE_CONTINUE; } $this->insertLine($this->_buffer); $tail = \mb_substr( $this->getLine(), $this->getLineCurrent() - 1 ); $this->_buffer = "\033[K".$tail.\str_repeat( "\033[D", \mb_strlen($tail) - 1 ); return static::STATE_CONTINUE; } /** * Add mappings. */ public function addMappings(array $mappings) { foreach ($mappings as $key => $mapping) { $this->addMapping($key, $mapping); } } /** * Add a mapping. * Supported key: * • \e[… for \033[…; * • \C-… for Ctrl-…; * • abc for a simple mapping. * A mapping is a callable that has only one parameter of type * Hoa\Console\Readline and that returns a self::STATE_* constant. */ public function addMapping(string $key, $mapping) { if ('\e[' === \substr($key, 0, 3)) { $this->_mapping["\033[".\substr($key, 3)] = $mapping; } elseif ('\C-' === \substr($key, 0, 3)) { $_key = \ord(\strtolower(\substr($key, 3))) - 96; $this->_mapping[\chr($_key)] = $mapping; } else { $this->_mapping[$key] = $mapping; } } /** * Add an entry in the history. */ public function addHistory(string $line = null) { if (empty($line)) { return; } $this->_history[] = $line; $this->_historyCurrent = $this->_historySize++; } /** * Clear history. */ public function clearHistory() { unset($this->_history); $this->_history = []; $this->_historyCurrent = 0; $this->_historySize = 1; } /** * Get an entry in the history. */ public function getHistory(int $i = null) { if (null === $i) { $i = $this->_historyCurrent; } if (!isset($this->_history[$i])) { return null; } return $this->_history[$i]; } /** * Go backward in the history. */ public function previousHistory() { if (0 >= $this->_historyCurrent) { return $this->getHistory(0); } return $this->getHistory($this->_historyCurrent--); } /** * Go forward in the history. */ public function nextHistory() { if ($this->_historyCurrent + 1 >= $this->_historySize) { return $this->getLine(); } return $this->getHistory(++$this->_historyCurrent); } /** * Get current line. */ public function getLine() { return $this->_line; } /** * Append to current line. */ public function appendLine(string $append) { $this->_line .= $append; $this->_lineLength = \mb_strlen($this->_line); $this->_lineCurrent = $this->_lineLength; } /** * Insert into current line at the current seek. */ public function insertLine(string $insert) { if ($this->_lineLength === $this->_lineCurrent) { return $this->appendLine($insert); } $this->_line = \mb_substr($this->_line, 0, $this->_lineCurrent). $insert. \mb_substr($this->_line, $this->_lineCurrent); $this->_lineLength = \mb_strlen($this->_line); $this->_lineCurrent += \mb_strlen($insert); return; } /** * Reset current line. */ protected function resetLine() { $this->_line = null; $this->_lineCurrent = 0; $this->_lineLength = 0; } /** * Get current line seek. */ public function getLineCurrent(): int { return $this->_lineCurrent; } /** * Get current line length. * * @return int */ public function getLineLength(): int { return $this->_lineLength; } /** * Set prefix. */ public function setPrefix(string $prefix) { $this->_prefix = $prefix; } /** * Get prefix. */ public function getPrefix() { return $this->_prefix; } /** * Get buffer. Not for user. */ public function getBuffer() { return $this->_buffer; } /** * Set an autocompleter. */ public function setAutocompleter(Autocompleter $autocompleter) { $old = $this->_autocompleter; $this->_autocompleter = $autocompleter; return $old; } /** * Get the autocompleter. * * @return ?Autocompleter */ public function getAutocompleter() { return $this->_autocompleter; } /** * Read on input. Not for user. */ public function _read(int $length = 512): string { return Console::getInput()->read($length); } /** * Set current line. Not for user. */ public function setLine(string $line) { $this->_line = $line; $this->_lineLength = \mb_strlen($this->_line ?: ''); $this->_lineCurrent = $this->_lineLength; } /** * Set current line seek. Not for user. */ public function setLineCurrent(int $current) { $this->_lineCurrent = $current; } /** * Set line length. Not for user. */ public function setLineLength(int $length) { $this->_lineLength = $length; } /** * Set buffer. Not for user. */ public function setBuffer(string $buffer) { $this->_buffer = $buffer; } /** * Up arrow binding. * Go backward in the history. */ public function _bindArrowUp(self $self): int { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $buffer = $self->previousHistory() ?? ''; $self->setBuffer($buffer); $self->setLine($buffer); return static::STATE_CONTINUE; } /** * Down arrow binding. * Go forward in the history. */ public function _bindArrowDown(self $self): int { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $self->setBuffer($buffer = $self->nextHistory()); $self->setLine($buffer); return static::STATE_CONTINUE; } /** * Right arrow binding. * Move cursor to the right. */ public function _bindArrowRight(self $self): int { if ($self->getLineLength() > $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('→'); } $self->setLineCurrent($self->getLineCurrent() + 1); } $self->setBuffer(''); return static::STATE_CONTINUE; } /** * Left arrow binding. * Move cursor to the left. */ public function _bindArrowLeft(self $self): int { if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('←'); } $self->setLineCurrent($self->getLineCurrent() - 1); } $self->setBuffer(''); return static::STATE_CONTINUE; } /** * Backspace and Control-H binding. * Delete the first character at the right of the cursor. */ public function _bindBackspace(self $self): int { $buffer = ''; if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('←'); ConsoleCursor::clear('→'); } if ($self->getLineLength() === $current = $self->getLineCurrent()) { $self->setLine(\mb_substr($self->getLine(), 0, -1)); } else { $line = $self->getLine(); $current = $self->getLineCurrent(); $tail = \mb_substr($line, $current); $buffer = $tail.\str_repeat("\033[D", \mb_strlen($tail)); $self->setLine(\mb_substr($line, 0, $current - 1).$tail); $self->setLineCurrent($current - 1); } } $self->setBuffer($buffer); return static::STATE_CONTINUE; } /** * Control-A binding. * Move cursor to beginning of line. */ public function _bindControlA(self $self): int { for ($i = $self->getLineCurrent() - 1; 0 <= $i; --$i) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } /** * Control-B binding. * Move cursor backward one word. */ public function _bindControlB(self $self): int { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ) { } for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } /** * Control-E binding. * Move cursor to end of line. */ public function _bindControlE(self $self): int { for ( $i = $self->getLineCurrent(), $max = $self->getLineLength(); $i < $max; ++$i ) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } /** * Control-F binding. * Move cursor forward one word. */ public function _bindControlF(self $self): int { $current = $self->getLineCurrent(); if ($self->getLineLength() === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i][1] < $current; ++$i ) { } if (!isset($words[$i + 1])) { $words[$i + 1] = [1 => $self->getLineLength()]; } for ($j = $words[$i + 1][1]; $j > $current; --$j) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } /** * Control-W binding. * Delete first backward word. */ public function _bindControlW(self $self): int { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ) { } for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindBackspace($self); } return static::STATE_CONTINUE; } /** * Newline binding. */ public function _bindNewline(self $self): int { $self->addHistory($self->getLine()); return static::STATE_BREAK; } /** * Tab binding. */ public function _bindTab(self $self): int { $output = Console::getOutput(); $autocompleter = $self->getAutocompleter(); $state = static::STATE_CONTINUE | static::STATE_NO_ECHO; if (null === $autocompleter) { return $state; } $current = $self->getLineCurrent(); $line = $self->getLine(); if (0 === $current) { return $state; } $matches = \preg_match_all( '#'.$autocompleter->getWordDefinition().'$#u', \mb_substr($line, 0, $current), $words ); if (0 === $matches) { return $state; } $word = $words[0][0]; if ('' === \trim($word)) { return $state; } $solution = $autocompleter->complete($word); $length = \mb_strlen($word); if (null === $solution) { return $state; } if (\is_array($solution)) { $_solution = $solution; $count = \count($_solution) - 1; $cWidth = 0; $window = ConsoleWindow::getSize(); $wWidth = $window['x']; $cursor = ConsoleCursor::getPosition(); \array_walk($_solution, function (&$value) use (&$cWidth) { $handle = \mb_strlen($value); if ($handle > $cWidth) { $cWidth = $handle; } return; }); \array_walk($_solution, function (&$value) use (&$cWidth) { $handle = \mb_strlen($value); if ($handle >= $cWidth) { return; } $value .= \str_repeat(' ', $cWidth - $handle); return; }); $mColumns = (int) \floor($wWidth / ($cWidth + 2)); $mLines = (int) \ceil(($count + 1) / $mColumns); --$mColumns; $i = 0; if (0 > $window['y'] - $cursor['y'] - $mLines) { ConsoleWindow::scroll('↑', $mLines); ConsoleCursor::move('↑', $mLines); } ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::clear('↓'); foreach ($_solution as $j => $s) { $output->writeAll("\033[0m".$s."\033[0m"); if ($i++ < $mColumns) { $output->writeAll(' '); } else { $i = 0; if (isset($_solution[$j + 1])) { $output->writeAll("\n"); } } } ConsoleCursor::restore(); ConsoleCursor::show(); ++$mColumns; $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = $except = []; $mColumn = -1; $mLine = -1; $coord = -1; $unselect = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::move('→', $mColumn * ($cWidth + 2)); ConsoleCursor::move('↓', $mLine); $output->writeAll("\033[0m".$_solution[$coord]."\033[0m"); ConsoleCursor::restore(); ConsoleCursor::show(); return; }; $select = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::move('→', $mColumn * ($cWidth + 2)); ConsoleCursor::move('↓', $mLine); $output->writeAll("\033[7m".$_solution[$coord]."\033[0m"); ConsoleCursor::restore(); ConsoleCursor::show(); return; }; $init = function () use ( &$mColumn, &$mLine, &$coord, &$select ) { $mColumn = 0; $mLine = 0; $coord = 0; $select(); return; }; while (true) { @\stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } switch ($char = $self->_read()) { case "\033[A": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \max(0, $coord - $mColumns); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[B": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \min($count, $coord + $mColumns); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\t": case "\033[C": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \min($count, $coord + 1); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[D": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \max(0, $coord - 1); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\n": if (-1 !== $mColumn && -1 !== $mLine) { $tail = \mb_substr($line, $current); $current -= $length; $self->setLine( \mb_substr($line, 0, $current). $solution[$coord]. $tail ); $self->setLineCurrent( $current + \mb_strlen($solution[$coord]) ); ConsoleCursor::move('←', $length); $output->writeAll($solution[$coord]); ConsoleCursor::clear('→'); $output->writeAll($tail); ConsoleCursor::move('←', \mb_strlen($tail)); } // no break default: $mColumn = -1; $mLine = -1; $coord = -1; ConsoleCursor::save(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::clear('↓'); ConsoleCursor::restore(); if ("\033" !== $char && "\n" !== $char) { $self->setBuffer($char); return $self->_readLine($char); } break 2; } } return $state; } $tail = \mb_substr($line, $current); $current -= $length; $self->setLine( \mb_substr($line, 0, $current). $solution. $tail ); $self->setLineCurrent( $current + \mb_strlen($solution) ); ConsoleCursor::move('←', $length); $output->writeAll($solution); ConsoleCursor::clear('→'); $output->writeAll($tail); ConsoleCursor::move('←', \mb_strlen($tail)); return $state; } } /* * Advanced interaction. */ Console::advancedInteraction(); �����������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileDoesNotExistException.php����������������������������������������������������������0000644�����������������00000003521�15025056514�0014562 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Exception\FileDoesNotExist. * * Extending the \Hoa\File\Exception class. * * @license New BSD License */ class FileDoesNotExistException extends FileException { } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/IStream.php����������������������������������������������������������������������������0000644�����������������00000003507�15025056514�0011063 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Stream. * * Interface for all streams. */ interface IStream { /** * Get the current stream. */ public function getStream(); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/ConsoleInput.php�����������������������������������������������������������������������0000644�����������������00000010234�15025056514�0012134 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Console\Input. * * This interface represents the input of a program. Most of the time, this is * going to be `php://stdin` but it can be `/dev/tty` if the former has been * closed. */ class ConsoleInput implements StreamIn { /** * Real input stream. */ protected $_input = null; /** * Wraps an `Hoa\Stream\IStream\In` stream. */ public function __construct(StreamIn $input = null) { if (null === $input) { if (\defined('STDIN') && false !== @\stream_get_meta_data(\STDIN)) { $input = new FileRead('php://stdin'); } else { $input = new FileRead('/dev/tty'); } } $this->_input = $input; return; } /** * Get underlying stream. */ public function getStream(): StreamIn { return $this->_input; } /** * Test for end-of-file. */ public function eof(): bool { return $this->_input->eof(); } /** * Read n characters. */ public function read(int $length) { return $this->_input->read($length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->_input->readString($length); } /** * Read a character. */ public function readCharacter() { return $this->_input->readCharacter(); } /** * Read a boolean. */ public function readBoolean() { return $this->_input->readBoolean(); } /** * Read an integer. */ public function readInteger(int $length = 1) { return $this->_input->readInteger($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return $this->_input->readFloat($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray($argument = null) { return $this->_input->readArray($argument); } /** * Read a line. */ public function readLine() { return $this->_input->readLine(); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return $this->_input->readAll($offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return $this->_input->scanf($format); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Terminfo/78/xterm-256color�������������������������������������������������������������0000644�����������������00000006372�15025056514�0013465 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%�&��xterm-256color|xterm with 256 colors������������������������������P�����������&�*�.�9�J�L�P�W�Y�f�j�n�x�|������������������� #'-39?EINRW\`gnrz"$'y|~� !(/7?GOW_gov}'.5<CKS[cks{ $c�� �[%i%p1%d;%p2%dr�����[%i%p1%dG�[%i%p1%d;%p2%dH� ��[?25l��[?12l[?25h���[?12;25h���(0���[?1049h������[%p1%dX�(B�(B�[?1049l����[?5h$<100/>[?5l�[!p[?3;4l>���[3~�OB�OP�[21~�OQ�OR�OS�[15~�[17~�[18~�[19~�[20~�OH�[2~�OD�[6~�[5~�OC���OA�[?1l>�[?1h=�[?1034l�[?1034h�[%p1%dP�[%p1%dM�[%p1%dB�[%p1%d@�[%p1%dS�[%p1%dL�[%p1%dD�[%p1%dC�[%p1%dT�[%p1%dA����c�[!p[?3;4l>�8�[%i%p1%dd�7� �M�%?%p9%t(0%e(B%;[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m�H� �OE�``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~��[?7h�[?7l�OF�OM�[3;2~���[2;2~��[6;2~�[5;2~��[23~�[24~�����[15;2~�[17;2~�[18;2~�[19;2~�[20;2~�[21;2~�[23;2~�[24;2~�����[15;5~�[17;5~�[18;5~�[19;5~�[20;5~�[21;5~�[23;5~�[24;5~�����[15;6~�[17;6~�[18;6~�[19;6~�[20;6~�[21;6~�[23;6~�[24;6~�����[15;3~�[17;3~�[18;3~�[19;3~�[20;3~�[21;3~�[23;3~�[24;3~�����[%i%d;%dR��[?1;2c���]4;%p1%d;rgb:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\��[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m�[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m�l�m�����:�v��������#�*�1�8�?�F�M�T�[�b�i�p�w�~������������������� &-4;BIPW^e���� � ���� �%�)�.�3�8�=�B�H�N�T�Z�`�f�l�r�x�~����������������������� "',048[3;3~�[3;4~�[3;5~�[3;6~�[3;7~�����������������[2;3~�[2;4~�[2;5~�[2;6~�[2;7~������[6;3~�[6;4~�[6;5~�[6;6~�[6;7~�[5;3~�[5;4~�[5;5~�[5;6~�[5;7~������������AX�G0�E0�S0�kDC3�kDC4�kDC5�kDC6�kDC7�kDN�kDN3�kDN4�kDN5�kDN6�kDN7�kEND3�kEND4�kEND5�kEND6�kEND7�kHOM3�kHOM4�kHOM5�kHOM6�kHOM7�kIC3�kIC4�kIC5�kIC6�kIC7�kLFT3�kLFT4�kLFT5�kLFT6�kLFT7�kNXT3�kNXT4�kNXT5�kNXT6�kNXT7�kPRV3�kPRV4�kPRV5�kPRV6�kPRV7�kRIT3�kRIT4�kRIT5�kRIT6�kRIT7�kUP�kUP3�kUP4�kUP5�kUP6�kUP7�ka2�kb1�kb3�kc2�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Terminfo/78/xterm����������������������������������������������������������������������0000644�����������������00000006272�15025056514�0012113 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������0�&��lxterm|xterm terminal emulator (X Window System)������������������������������P����@��������&�*�.�9�J�L�P�W�Y�f�j�n�x�|������������������� #'-39?EINRW\`gnrz"$'y|~� !(/7?GOW_gov}'.5<CKS[cks{NR\fi�� �[%i%p1%d;%p2%dr�����[%i%p1%dG�[%i%p1%d;%p2%dH� ��[?25l��[?12l[?25h���[?12;25h���(0���[?1049h������[%p1%dX�(B�(B�[?1049l����[?5h$<100/>[?5l�[!p[?3;4l>���[3~�OB�OP�[21~�OQ�OR�OS�[15~�[17~�[18~�[19~�[20~�OH�[2~�OD�[6~�[5~�OC���OA�[?1l>�[?1h=�[?1034l�[?1034h�[%p1%dP�[%p1%dM�[%p1%dB�[%p1%d@�[%p1%dS�[%p1%dL�[%p1%dD�[%p1%dC�[%p1%dT�[%p1%dA����c�[!p[?3;4l>�8�[%i%p1%dd�7� �M�%?%p9%t(0%e(B%;[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m�H� �OE�``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~��[?7h�[?7l�OF�OM�[3;2~���[2;2~��[6;2~�[5;2~��[23~�[24~�����[15;2~�[17;2~�[18;2~�[19;2~�[20;2~�[21;2~�[23;2~�[24;2~�����[15;5~�[17;5~�[18;5~�[19;5~�[20;5~�[21;5~�[23;5~�[24;5~�����[15;6~�[17;6~�[18;6~�[19;6~�[20;6~�[21;6~�[23;6~�[24;6~�����[15;3~�[17;3~�[18;3~�[19;3~�[20;3~�[21;3~�[23;3~�[24;3~�����[%i%d;%dR��[?1;2c���[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m�[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m��[3%p1%dm�[4%p1%dm�l�m����9�s��������#�*�1�8�?�F�M�T�[�b�i�p�w�~������������������� &-4;BIPW^e���� �����#�(�-�2�7�<�B�H�N�T�Z�`�f�l�r�x�}�����������������������  !&*.2[3;3~�[3;4~�[3;5~�[3;6~�[3;7~�����������������[2;3~�[2;4~�[2;5~�[2;6~�[2;7~������[6;3~�[6;4~�[6;5~�[6;6~�[6;7~�[5;3~�[5;4~�[5;5~�[5;6~�[5;7~������������AX�XM�kDC3�kDC4�kDC5�kDC6�kDC7�kDN�kDN3�kDN4�kDN5�kDN6�kDN7�kEND3�kEND4�kEND5�kEND6�kEND7�kHOM3�kHOM4�kHOM5�kHOM6�kHOM7�kIC3�kIC4�kIC5�kIC6�kIC7�kLFT3�kLFT4�kLFT5�kLFT6�kLFT7�kNXT3�kNXT4�kNXT5�kNXT6�kNXT7�kPRV3�kPRV4�kPRV5�kPRV6�kPRV7�kRIT3�kRIT4�kRIT5�kRIT6�kRIT7�kUP�kUP3�kUP4�kUP5�kUP6�kUP7�ka2�kb1�kb3�kc2���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Terminfo/77/windows-ansi���������������������������������������������������������������0000644�����������������00000002711�15025056514�0013367 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������(�&��}Dansi|ansi/pc-term compatible with color����������������������������������P����@������� ����'�8�<�@�D�H�L�P�T�Z�_�d�i�n�s�x���������������������� 2=? (,048>�� �����[%i%p1%dG�[%i%p1%d;%p2%dH���������������[%p1%dX������������� �[%p1%dP�[%p1%dM�[%p1%dB�[%p1%d@�[%p1%dS�[%p1%dL�[%p1%dD�[%p1%dC�[%p1%dT�[%p1%dA���%p1%c[%p2%{1}%-%db�[%i%p1%dd� �[0;10%?%p1%t;7%;%?%p2%t;4%;%?%p3%t;7%;%?%p4%t;5%;%?%p6%t;1%;%?%p7%t;8%;%?%p9%t;11%;m�H��+,-.0`a-hjklmno~pqrs_tuvwxyz{|}~���[%i%d;%dR��[?%[;0123456789]c���[3%p1%dm�[4%p1%dm�(B�)B�*B�+B�������������AX��������������������������������������������������������Readline/Hoa/ConsoleProcessus.php�������������������������������������������������������������������0000644�����������������00000050674�15025056514�0013037 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Manipulate a processus as a stream. */ class ConsoleProcessus extends Stream implements StreamIn, StreamOut, StreamPathable { /** * Signal: terminal line hangup (terminate process). */ const SIGHUP = 1; /** * Signal: interrupt program (terminate process). */ const SIGINT = 2; /** * Signal: quit program (create core image). */ const SIGQUIT = 3; /** * Signal: illegal instruction (create core image). */ const SIGILL = 4; /** * Signal: trace trap (create core image). */ const SIGTRAP = 5; /** * Signal: abort program, formerly SIGIOT (create core image). */ const SIGABRT = 6; /** * Signal: emulate instruction executed (create core image). */ const SIGEMT = 7; /** * Signal: floating-point exception (create core image). */ const SIGFPE = 8; /** * Signal: kill program (terminate process). */ const SIGKILL = 9; /** * Signal: bus error. */ const SIGBUS = 10; /** * Signal: segmentation violation (create core image). */ const SIGSEGV = 11; /** * Signal: non-existent system call invoked (create core image). */ const SIGSYS = 12; /** * Signal: write on a pipe with no reader (terminate process). */ const SIGPIPE = 13; /** * Signal: real-time timer expired (terminate process). */ const SIGALRM = 14; /** * Signal: software termination signal (terminate process). */ const SIGTERM = 15; /** * Signal: urgent condition present on socket (discard signal). */ const SIGURG = 16; /** * Signal: stop, cannot be caught or ignored (stop proces). */ const SIGSTOP = 17; /** * Signal: stop signal generated from keyboard (stop process). */ const SIGTSTP = 18; /** * Signal: continue after stop (discard signal). */ const SIGCONT = 19; /** * Signal: child status has changed (discard signal). */ const SIGCHLD = 20; /** * Signal: background read attempted from control terminal (stop process). */ const SIGTTIN = 21; /** * Signal: background write attempted to control terminal (stop process). */ const SIGTTOU = 22; /** * Signal: I/O is possible on a descriptor, see fcntl(2) (discard signal). */ const SIGIO = 23; /** * Signal: cpu time limit exceeded, see setrlimit(2) (terminate process). */ const SIGXCPU = 24; /** * Signal: file size limit exceeded, see setrlimit(2) (terminate process). */ const SIGXFSZ = 25; /** * Signal: virtual time alarm, see setitimer(2) (terminate process). */ const SIGVTALRM = 26; /** * Signal: profiling timer alarm, see setitimer(2) (terminate process). */ const SIGPROF = 27; /** * Signal: Window size change (discard signal). */ const SIGWINCH = 28; /** * Signal: status request from keyboard (discard signal). */ const SIGINFO = 29; /** * Signal: User defined signal 1 (terminate process). */ const SIGUSR1 = 30; /** * Signal: User defined signal 2 (terminate process). */ const SIGUSR2 = 31; /** * Command name. */ protected $_command = null; /** * Command options (options => value, or input). */ protected $_options = []; /** * Current working directory. */ protected $_cwd = null; /** * Environment. */ protected $_environment = null; /** * Timeout. */ protected $_timeout = 30; /** * Descriptor. */ protected $_descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; /** * Pipe descriptors of the processus. */ protected $_pipes = null; /** * Seekability of pipes. */ protected $_seekable = []; /** * Start a processus. */ public function __construct( string $command, array $options = null, array $descriptors = null, string $cwd = null, array $environment = null, int $timeout = 30 ) { $this->setCommand($command); if (null !== $options) { $this->setOptions($options); } if (null !== $descriptors) { $this->_descriptors = []; foreach ($descriptors as $descriptor => $nature) { if (isset($this->_descriptors[$descriptor])) { throw new ConsoleException('Pipe descriptor %d already exists, cannot '.'redefine it.', 0, $descriptor); } $this->_descriptors[$descriptor] = $nature; } } $this->setCwd($cwd ?: \getcwd()); if (null !== $environment) { $this->setEnvironment($environment); } $this->setTimeout($timeout); parent::__construct($this->getCommandLine(), null, true); $this->getListener()->addIds(['input', 'output', 'timeout', 'start', 'stop']); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { $out = @\proc_open( $streamName, $this->_descriptors, $this->_pipes, $this->getCwd(), $this->getEnvironment() ); if (false === $out) { throw new ConsoleException('Something wrong happen when running %s.', 1, $streamName); } return $out; } /** * Close the current stream. */ protected function _close(): bool { foreach ($this->_pipes as $pipe) { @\fclose($pipe); } return (bool) @\proc_close($this->getStream()); } /** * Run the process and fire events (amongst start, stop, input, output and * timeout). * If an event returns false, it will close the current pipe. * For a simple run without firing events, use the $this->open() method. */ public function run() { if (false === $this->isOpened()) { $this->open(); } else { $this->_close(); $this->_setStream($this->_open( $this->getStreamName(), $this->getStreamContext() )); } $this->getListener()->fire('start', new EventBucket()); $_read = []; $_write = []; $_except = []; foreach ($this->_pipes as $p => $pipe) { switch ($this->_descriptors[$p][1]) { case 'r': \stream_set_blocking($pipe, false); $_write[] = $pipe; break; case 'w': case 'a': \stream_set_blocking($pipe, true); $_read[] = $pipe; break; } } while (true) { foreach ($_read as $i => $r) { if (false === \is_resource($r)) { unset($_read[$i]); } } foreach ($_write as $i => $w) { if (false === \is_resource($w)) { unset($_write[$i]); } } foreach ($_except as $i => $e) { if (false === \is_resource($e)) { unset($_except[$i]); } } if (empty($_read) && empty($_write) && empty($_except)) { break; } $read = $_read; $write = $_write; $except = $_except; $select = \stream_select($read, $write, $except, $this->getTimeout()); if (0 === $select) { $this->getListener()->fire('timeout', new EventBucket()); break; } foreach ($read as $i => $_r) { $pipe = \array_search($_r, $this->_pipes); $line = $this->readLine($pipe); if (false === $line) { $result = [false]; } else { $result = $this->getListener()->fire( 'output', new EventBucket([ 'pipe' => $pipe, 'line' => $line, ]) ); } if (true === \feof($_r) || \in_array(false, $result, true)) { \fclose($_r); unset($_read[$i]); break; } } foreach ($write as $j => $_w) { $result = $this->getListener()->fire( 'input', new EventBucket([ 'pipe' => \array_search($_w, $this->_pipes), ]) ); if (true === \feof($_w) || \in_array(false, $result, true)) { \fclose($_w); unset($_write[$j]); } } if (empty($_read)) { break; } } $this->getListener()->fire('stop', new EventBucket()); return; } /** * Get pipe resource. */ protected function getPipe(int $pipe) { if (!isset($this->_pipes[$pipe])) { throw new ConsoleException('Pipe descriptor %d does not exist, cannot read from it.', 2, $pipe); } return $this->_pipes[$pipe]; } /** * Check if a pipe is seekable or not. */ protected function isPipeSeekable(int $pipe): bool { if (!isset($this->_seekable[$pipe])) { $_pipe = $this->getPipe($pipe); $data = \stream_get_meta_data($_pipe); $this->_seekable[$pipe] = $data['seekable']; } return $this->_seekable[$pipe]; } /** * Test for end-of-file. */ public function eof(int $pipe = 1): bool { return \feof($this->getPipe($pipe)); } /** * Read n characters. */ public function read(int $length, int $pipe = 1) { if (0 > $length) { throw new ConsoleException('Length must be greater than 0, given %d.', 3, $length); } return \fread($this->getPipe($pipe), $length); } /** * Alias of $this->read(). */ public function readString(int $length, int $pipe = 1) { return $this->read($length, $pipe); } /** * Read a character. */ public function readCharacter(int $pipe = 1) { return \fgetc($this->getPipe($pipe)); } /** * Read a boolean. */ public function readBoolean(int $pipe = 1) { return (bool) $this->read(1, $pipe); } /** * Read an integer. */ public function readInteger(int $length = 1, int $pipe = 1) { return (int) $this->read($length, $pipe); } /** * Read a float. */ public function readFloat(int $length = 1, int $pipe = 1) { return (float) $this->read($length, $pipe); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null, int $pipe = 1) { return $this->scanf($format, $pipe); } /** * Read a line. */ public function readLine(int $pipe = 1) { return \stream_get_line($this->getPipe($pipe), 1 << 15, "\n"); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = -1, int $pipe = 1) { $_pipe = $this->getPipe($pipe); if (true === $this->isPipeSeekable($pipe)) { $offset += \ftell($_pipe); } else { $offset = -1; } return \stream_get_contents($_pipe, -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format, int $pipe = 1): array { return \fscanf($this->getPipe($pipe), $format); } /** * Write n characters. */ public function write(string $string, int $length, int $pipe = 0) { if (0 > $length) { throw new ConsoleException('Length must be greater than 0, given %d.', 4, $length); } return \fwrite($this->getPipe($pipe), $string, $length); } /** * Write a string. */ public function writeString(string $string, int $pipe = 0) { $string = (string) $string; return $this->write($string, \strlen($string), $pipe); } /** * Write a character. */ public function writeCharacter(string $char, int $pipe = 0) { return $this->write((string) $char[0], 1, $pipe); } /** * Write a boolean. */ public function writeBoolean(bool $boolean, int $pipe = 0) { return $this->write((string) (bool) $boolean, 1, $pipe); } /** * Write an integer. */ public function writeInteger(int $integer, int $pipe = 0) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer), $pipe); } /** * Write a float. */ public function writeFloat(float $float, int $pipe = 0) { $float = (string) (float) $float; return $this->write($float, \strlen($float), $pipe); } /** * Write an array. */ public function writeArray(array $array, int $pipe = 0) { $array = \var_export($array, true); return $this->write($array, \strlen($array), $pipe); } /** * Write a line. */ public function writeLine(string $line, int $pipe = 0) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1, $pipe); } ++$n; return $this->write(\substr($line, 0, $n), $n, $pipe); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string, int $pipe = 0) { return $this->write($string, \strlen($string), $pipe); } /** * Truncate a file to a given length. */ public function truncate(int $size, int $pipe = 0): bool { return \ftruncate($this->getPipe($pipe), $size); } /** * Get filename component of path. */ public function getBasename(): string { return \basename($this->getCommand()); } /** * Get directory name component of path. */ public function getDirname(): string { return \dirname($this->getCommand()); } /** * Get status. */ public function getStatus(): array { return \proc_get_status($this->getStream()); } /** * Get exit code (alias of $this->getStatus()['exitcode']);. */ public function getExitCode(): int { $handle = $this->getStatus(); return $handle['exitcode']; } /** * Whether the processus have ended successfully. * * @return bool */ public function isSuccessful(): bool { return 0 === $this->getExitCode(); } /** * Terminate the process. * * Valid signals are self::SIGHUP, SIGINT, SIGQUIT, SIGABRT, SIGKILL, * SIGALRM and SIGTERM. */ public function terminate(int $signal = self::SIGTERM): bool { return \proc_terminate($this->getStream(), $signal); } /** * Set command name. */ protected function setCommand(string $command) { $old = $this->_command; $this->_command = \escapeshellcmd($command); return $old; } /** * Get command name. */ public function getCommand() { return $this->_command; } /** * Set command options. */ protected function setOptions(array $options): array { foreach ($options as &$option) { $option = \escapeshellarg($option); } $old = $this->_options; $this->_options = $options; return $old; } /** * Get options. */ public function getOptions(): array { return $this->_options; } /** * Get command-line. */ public function getCommandLine(): string { $out = $this->getCommand(); foreach ($this->getOptions() as $key => $value) { if (!\is_int($key)) { $out .= ' '.$key.'='.$value; } else { $out .= ' '.$value; } } return $out; } /** * Set current working directory of the process. */ protected function setCwd(string $cwd) { $old = $this->_cwd; $this->_cwd = $cwd; return $old; } /** * Get current working directory of the process. */ public function getCwd(): string { return $this->_cwd; } /** * Set environment of the process. */ protected function setEnvironment(array $environment) { $old = $this->_environment; $this->_environment = $environment; return $old; } /** * Get environment of the process. */ public function getEnvironment() { return $this->_environment; } /** * Set timeout of the process. */ public function setTimeout(int $timeout) { $old = $this->_timeout; $this->_timeout = $timeout; return $old; } /** * Get timeout of the process. */ public function getTimeout(): int { return $this->_timeout; } /** * Set process title. */ public static function setTitle(string $title) { \cli_set_process_title($title); } /** * Get process title. */ public static function getTitle() { return \cli_get_process_title(); } /** * Found the place of a binary. */ public static function locate(string $binary) { if (isset($_ENV['PATH'])) { $separator = ':'; $path = &$_ENV['PATH']; } elseif (isset($_SERVER['PATH'])) { $separator = ':'; $path = &$_SERVER['PATH']; } elseif (isset($_SERVER['Path'])) { $separator = ';'; $path = &$_SERVER['Path']; } else { return null; } foreach (\explode($separator, $path) as $directory) { if (true === \file_exists($out = $directory.\DIRECTORY_SEPARATOR.$binary)) { return $out; } } return null; } /** * Quick process execution. * Returns only the STDOUT. */ public static function execute(string $commandLine, bool $escape = true): string { if (true === $escape) { $commandLine = \escapeshellcmd($commandLine); } return \rtrim(\shell_exec($commandLine) ?? ''); } } ��������������������������������������������������������������������Readline/Hoa/FileDirectory.php����������������������������������������������������������������������0000644�����������������00000014525�15025056514�0012265 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Directory. * * Directory handler. */ class FileDirectory extends FileGeneric { /** * Open for reading. */ const MODE_READ = 'rb'; /** * Open for reading and writing. If the directory does not exist, attempt to * create it. */ const MODE_CREATE = 'xb'; /** * Open for reading and writing. If the directory does not exist, attempt to * create it recursively. */ const MODE_CREATE_RECURSIVE = 'xrb'; /** * Open a directory. */ public function __construct( string $streamName, string $mode = self::MODE_READ, string $context = null, bool $wait = false ) { $this->setMode($mode); parent::__construct($streamName, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { if (false === \is_dir($streamName)) { if ($this->getMode() === self::MODE_READ) { throw new FileDoesNotExistException('Directory %s does not exist.', 0, $streamName); } else { self::create( $streamName, $this->getMode(), null !== $context ? $context->getContext() : null ); } } $out = null; return $out; } /** * Close the current stream. */ protected function _close(): bool { return true; } /** * Recursive copy of a directory. */ public function copy(string $to, bool $force = StreamTouchable::DO_NOT_OVERWRITE): bool { if (empty($to)) { throw new FileException('The destination path (to copy) is empty.', 1); } $from = $this->getStreamName(); $fromLength = \strlen($from) + 1; $finder = new FileFinder(); $finder->in($from); self::create($to, self::MODE_CREATE_RECURSIVE); foreach ($finder as $file) { $relative = \substr($file->getPathname(), $fromLength); $_to = $to.\DIRECTORY_SEPARATOR.$relative; if (true === $file->isDir()) { self::create($_to, self::MODE_CREATE); continue; } // This is not possible to do `$file->open()->copy(); // $file->close();` because the file will be opened in read and // write mode. In a PHAR for instance, this operation is // forbidden. So a special care must be taken to open file in read // only mode. $handle = null; if (true === $file->isFile()) { $handle = new FileRead($file->getPathname()); } elseif (true === $file->isDir()) { $handle = new self($file->getPathName()); } elseif (true === $file->isLink()) { $handle = new FileLinkRead($file->getPathName()); } if (null !== $handle) { $handle->copy($_to, $force); $handle->close(); } } return true; } /** * Delete a directory. */ public function delete(): bool { $from = $this->getStreamName(); $finder = new FileFinder(); $finder->in($from) ->childFirst(); foreach ($finder as $file) { $file->open()->delete(); $file->close(); } if (null === $this->getStreamContext()) { return @\rmdir($from); } return @\rmdir($from, $this->getStreamContext()->getContext()); } /** * Create a directory. */ public static function create( string $name, string $mode = self::MODE_CREATE_RECURSIVE, string $context = null ): bool { if (true === \is_dir($name)) { return true; } if (empty($name)) { return false; } if (null !== $context) { if (false === StreamContext::contextExists($context)) { throw new FileException('Context %s was not previously declared, cannot retrieve '.'this context.', 2, $context); } else { $context = StreamContext::getInstance($context); } } if (null === $context) { return @\mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode ); } return @\mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode, $context->getContext() ); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/ConsoleException.php�������������������������������������������������������������������0000644�����������������00000003431�15025056514�0012774 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Exception. * * Extending the \Hoa\Exception\Exception class. */ class ConsoleException extends Exception { } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/EventBucket.php������������������������������������������������������������������������0000644�����������������00000005601�15025056514�0011733 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * This class is the object which is transmit through event channels. */ class EventBucket { /** * The source object (must be of kind `Hoa\Event\Source`). */ protected $_source = null; /** * Data attached to the bucket. */ protected $_data = null; /** * Allocates a new bucket with various data attached to it. */ public function __construct($data = null) { $this->setData($data); return; } /** * Sends this object on the event channel. */ public function send(string $eventId, EventSource $source) { return Event::notify($eventId, $source, $this); } /** * Sets a new source. */ public function setSource(EventSource $source) { $old = $this->_source; $this->_source = $source; return $old; } /** * Returns the source. */ public function getSource() { return $this->_source; } /** * Sets new data. */ public function setData($data) { $old = $this->_data; $this->_data = $data; return $old; } /** * Returns the data. */ public function getData() { return $this->_data; } } �������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/IteratorRecursiveDirectory.php���������������������������������������������������������0000644�����������������00000007322�15025056514�0015064 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Iterator\Recursive\Directory. * * Extending the SPL RecursiveDirectoryIterator class. */ class IteratorRecursiveDirectory extends \RecursiveDirectoryIterator { /** * SplFileInfo classname. */ protected $_splFileInfoClass = null; /** * Relative path. */ protected $_relativePath = null; /** * Constructor. * Please, see \RecursiveDirectoryIterator::__construct() method. * We add the $splFileInfoClass parameter. */ public function __construct(string $path, int $flags = null, string $splFileInfoClass = null) { if (null === $flags) { parent::__construct($path); } else { parent::__construct($path, $flags); } $this->_relativePath = $path; $this->setSplFileInfoClass($splFileInfoClass); return; } /** * Current. * Please, see \RecursiveDirectoryIterator::current() method. */ #[\ReturnTypeWillChange] public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); if ($out instanceof IteratorSplFileInfo) { $out->setRelativePath($this->getRelativePath()); } } return $out; } /** * Get children. * Please, see \RecursiveDirectoryIterator::getChildren() method. */ #[\ReturnTypeWillChange] public function getChildren() { $out = parent::getChildren(); $out->_relativePath = $this->getRelativePath(); $out->setSplFileInfoClass($this->_splFileInfoClass); return $out; } /** * Set SplFileInfo classname. */ public function setSplFileInfoClass($splFileInfoClass) { $this->_splFileInfoClass = $splFileInfoClass; } /** * Get relative path (if given). */ public function getRelativePath(): string { return $this->_relativePath; } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/File.php�������������������������������������������������������������������������������0000644�����������������00000017255�15025056514�0010403 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File. * * File handler. */ abstract class File extends FileGeneric implements StreamBufferable, StreamLockable, StreamPointable { /** * Open for reading only; place the file pointer at the beginning of the * file. */ const MODE_READ = 'rb'; /** * Open for reading and writing; place the file pointer at the beginning of * the file. */ const MODE_READ_WRITE = 'r+b'; /** * Open for writing only; place the file pointer at the beginning of the * file and truncate the file to zero length. If the file does not exist, * attempt to create it. */ const MODE_TRUNCATE_WRITE = 'wb'; /** * Open for reading and writing; place the file pointer at the beginning of * the file and truncate the file to zero length. If the file does not * exist, attempt to create it. */ const MODE_TRUNCATE_READ_WRITE = 'w+b'; /** * Open for writing only; place the file pointer at the end of the file. If * the file does not exist, attempt to create it. */ const MODE_APPEND_WRITE = 'ab'; /** * Open for reading and writing; place the file pointer at the end of the * file. If the file does not exist, attempt to create it. */ const MODE_APPEND_READ_WRITE = 'a+b'; /** * Create and open for writing only; place the file pointer at the beginning * of the file. If the file already exits, the fopen() call with fail by * returning false and generating an error of level E_WARNING. If the file * does not exist, attempt to create it. This is equivalent to specifying * O_EXCL | O_CREAT flags for the underlying open(2) system call. */ const MODE_CREATE_WRITE = 'xb'; /** * Create and open for reading and writing; place the file pointer at the * beginning of the file. If the file already exists, the fopen() call with * fail by returning false and generating an error of level E_WARNING. If * the file does not exist, attempt to create it. This is equivalent to * specifying O_EXCL | O_CREAT flags for the underlying open(2) system call. */ const MODE_CREATE_READ_WRITE = 'x+b'; /** * Open a file. */ public function __construct( string $streamName, string $mode, string $context = null, bool $wait = false ) { $this->setMode($mode); switch ($streamName) { case '0': $streamName = 'php://stdin'; break; case '1': $streamName = 'php://stdout'; break; case '2': $streamName = 'php://stderr'; break; default: if (true === \ctype_digit($streamName)) { if (\PHP_VERSION_ID >= 50306) { $streamName = 'php://fd/'.$streamName; } else { throw new FileException('You need PHP5.3.6 to use a file descriptor '.'other than 0, 1 or 2 (tried %d with PHP%s).', 0, [$streamName, \PHP_VERSION]); } } } parent::__construct($streamName, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { if (\substr($streamName, 0, 4) === 'file' && false === \is_dir(\dirname($streamName))) { throw new FileException('Directory %s does not exist. Could not open file %s.', 1, [\dirname($streamName), \basename($streamName)]); } if (null === $context) { if (false === $out = @\fopen($streamName, $this->getMode(), true)) { throw new FileException('Failed to open stream %s.', 2, $streamName); } return $out; } $out = @\fopen( $streamName, $this->getMode(), true, $context->getContext() ); if (false === $out) { throw new FileException('Failed to open stream %s.', 3, $streamName); } return $out; } /** * Close the current stream. */ protected function _close(): bool { return @\fclose($this->getStream()); } /** * Start a new buffer. * The callable acts like a light filter. */ public function newBuffer($callable = null, int $size = null): int { $this->setStreamBuffer($size); // @TODO manage $callable as a filter? return 1; } /** * Flush the output to a stream. */ public function flush(): bool { return \fflush($this->getStream()); } /** * Delete buffer. */ public function deleteBuffer(): bool { return $this->disableStreamBuffer(); } /** * Get bufffer level. */ public function getBufferLevel(): int { return 1; } /** * Get buffer size. */ public function getBufferSize(): int { return $this->getStreamBufferSize(); } /** * Portable advisory locking. */ public function lock(int $operation): bool { return \flock($this->getStream(), $operation); } /** * Rewind the position of a stream pointer. */ public function rewind(): bool { return \rewind($this->getStream()); } /** * Seek on a stream pointer. */ public function seek(int $offset, int $whence = StreamPointable::SEEK_SET): int { return \fseek($this->getStream(), $offset, $whence); } /** * Get the current position of the stream pointer. */ public function tell(): int { $stream = $this->getStream(); if (null === $stream) { return 0; } return \ftell($stream); } /** * Create a file. */ public static function create(string $name) { if (\file_exists($name)) { return true; } return \touch($name); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/EventListener.php����������������������������������������������������������������������0000644�����������������00000007762�15025056514�0012315 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * A contrario of events, listeners are synchronous, identified at use and * useful for close interactions between one or some components. */ class EventListener { /** * Source of listener (for `Hoa\Event\Bucket`). */ protected $_source = null; /** * All listener IDs and associated listeners. */ protected $_callables = []; /** * Build a listener. */ public function __construct(EventListenable $source, array $ids) { $this->_source = $source; $this->addIds($ids); return; } /** * Adds acceptable ID (or reset). */ public function addIds(array $ids) { foreach ($ids as $id) { $this->_callables[$id] = []; } } /** * Attaches a callable to a listenable component. */ public function attach(string $listenerId, $callable): self { if (false === $this->listenerExists($listenerId)) { throw new EventException('Cannot listen %s because it is not defined.', 0, $listenerId); } $callable = Xcallable::from($callable); $this->_callables[$listenerId][$callable->getHash()] = $callable; return $this; } /** * Detaches a callable from a listenable component. */ public function detach(string $listenerId, $callable): self { unset($this->_callables[$listenerId][Xcallable::from($callable)->getHash()]); return $this; } /** * Detaches all callables from a listenable component. */ public function detachAll(string $listenerId): self { unset($this->_callables[$listenerId]); return $this; } /** * Checks if a listener exists. */ public function listenerExists(string $listenerId): bool { return \array_key_exists($listenerId, $this->_callables); } /** * Sends/fires a bucket to a listener. */ public function fire(string $listenerId, EventBucket $data): array { if (false === $this->listenerExists($listenerId)) { throw new EventException('Cannot fire on %s because it is not defined.', 1, $listenerId); } $data->setSource($this->_source); $out = []; foreach ($this->_callables[$listenerId] as $callable) { $out[] = $callable($data); } return $out; } } ��������������Readline/Hoa/Event.php������������������������������������������������������������������������������0000644�����������������00000013705�15025056514�0010601 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Events are asynchronous at registration, anonymous at use (until we * receive a bucket) and useful to largely spread data through components * without any known connection between them. */ class Event { /** * Event ID key. */ const KEY_EVENT = 0; /** * Source object key. */ const KEY_SOURCE = 1; /** * Static register of all observable objects, i.e. `Hoa\Event\Source` * object, i.e. object that can send event. */ private static $_register = []; /** * Collection of callables, i.e. observer objects. */ protected $_callable = []; /** * Privatize the constructor. */ private function __construct() { return; } /** * Manage multiton of events, with the principle of asynchronous * attachments. */ public static function getEvent(string $eventId): self { if (!isset(self::$_register[$eventId][self::KEY_EVENT])) { self::$_register[$eventId] = [ self::KEY_EVENT => new self(), self::KEY_SOURCE => null, ]; } return self::$_register[$eventId][self::KEY_EVENT]; } /** * Declares a new object in the observable collection. * Note: Hoa's libraries use `hoa://Event/anID` for their observable objects. */ public static function register(string $eventId, /* Source|string */ $source) { if (true === self::eventExists($eventId)) { throw new EventException('Cannot redeclare an event with the same ID, i.e. the event '.'ID %s already exists.', 0, $eventId); } if (\is_object($source) && !($source instanceof EventSource)) { throw new EventException('The source must implement \Hoa\Event\Source '.'interface; given %s.', 1, \get_class($source)); } else { $reflection = new \ReflectionClass($source); if (false === $reflection->implementsInterface('\Psy\Readline\Hoa\EventSource')) { throw new EventException('The source must implement \Hoa\Event\Source '.'interface; given %s.', 2, $source); } } if (!isset(self::$_register[$eventId][self::KEY_EVENT])) { self::$_register[$eventId][self::KEY_EVENT] = new self(); } self::$_register[$eventId][self::KEY_SOURCE] = $source; } /** * Undeclares an object in the observable collection. * * If `$hard` is set to `true, then the source and its attached callables * will be deleted. */ public static function unregister(string $eventId, bool $hard = false) { if (false !== $hard) { unset(self::$_register[$eventId]); } else { self::$_register[$eventId][self::KEY_SOURCE] = null; } } /** * Attach an object to an event. * * It can be a callable or an accepted callable form (please, see the * `Hoa\Consistency\Xcallable` class). */ public function attach($callable): self { $callable = Xcallable::from($callable); $this->_callable[$callable->getHash()] = $callable; return $this; } /** * Detaches an object to an event. * * Please see `self::attach` method. */ public function detach($callable): self { unset($this->_callable[Xcallable::from($callable)->getHash()]); return $this; } /** * Checks if at least one callable is attached to an event. */ public function isListened(): bool { return !empty($this->_callable); } /** * Notifies, i.e. send data to observers. */ public static function notify(string $eventId, EventSource $source, EventBucket $data) { if (false === self::eventExists($eventId)) { throw new EventException('Event ID %s does not exist, cannot send notification.', 3, $eventId); } $data->setSource($source); $event = self::getEvent($eventId); foreach ($event->_callable as $callable) { $callable($data); } } /** * Checks whether an event exists. */ public static function eventExists(string $eventId): bool { return \array_key_exists($eventId, self::$_register) && self::$_register[$eventId][self::KEY_SOURCE] !== null; } } �����������������������������������������������������������Readline/Hoa/EventListens.php�����������������������������������������������������������������������0000644�����������������00000005130�15025056514�0012134 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Implementation of a listener. */ trait EventListens { /** * Listener instance of type `Hoa\Event\Listener`. */ protected $_listener = null; /** * Attaches a callable to a listenable component. */ public function on(string $listenerId, $callable): EventListenable { $listener = $this->getListener(); if (null === $listener) { throw new EventException('Cannot attach a callable to the listener %s because '.'it has not been initialized yet.', 0, static::class); } $listener->attach($listenerId, $callable); return $this; } /** * Sets a new listener. */ protected function setListener(EventListener $listener) { $old = $this->_listener; $this->_listener = $listener; return $old; } /** * Returns the listener. */ protected function getListener() { return $this->_listener; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/IteratorSplFileInfo.php����������������������������������������������������������������0000644�����������������00000006336�15025056514�0013406 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Iterator\SplFileInfo. * * Enhance SplFileInfo implementation. */ class IteratorSplFileInfo extends \SplFileInfo { /** * Hash. */ protected $_hash = null; /** * Relative path. */ protected $_relativePath = null; /** * Construct. */ public function __construct(string $filename, string $relativePath = null) { parent::__construct($filename); if (-1 !== $mtime = $this->getMTime()) { $this->_hash = \md5($this->getPathname().$mtime); } $this->_relativePath = $relativePath; return; } /** * Get the hash. */ public function getHash(): string { return $this->_hash; } /** * Get the MTime. */ public function getMTime(): int { try { return parent::getMTime(); } catch (\RuntimeException $e) { return -1; } } /** * Set relative path. */ public function setRelativePath(string $relativePath) { $old = $this->_relativePath; $this->_relativePath = $relativePath; return $old; } /** * Get relative path (if given). */ public function getRelativePath() { return $this->_relativePath; } /** * Get relative pathname (if possible). */ public function getRelativePathname(): string { if (null === $relative = $this->getRelativePath()) { return $this->getPathname(); } return \substr($this->getPathname(), \strlen($relative)); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/EventListenable.php��������������������������������������������������������������������0000644�����������������00000003616�15025056514�0012604 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Each object which is listenable must implement this interface. */ interface EventListenable extends EventSource { /** * Attaches a callable to a listenable component. */ public function on(string $listenerId, $callable): self; } ������������������������������������������������������������������������������������������������������������������Readline/Hoa/AutocompleterWord.php������������������������������������������������������������������0000644�����������������00000006153�15025056514�0013176 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Readline\Autocompleter\Word. * * The simplest auto-completer: complete a word. */ class AutocompleterWord implements Autocompleter { /** * List of words. */ protected $_words = null; /** * Constructor. */ public function __construct(array $words) { $this->setWords($words); } /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. * * @param string &$prefix Prefix to autocomplete * * @return mixed */ public function complete(&$prefix) { $out = []; $length = \mb_strlen($prefix); foreach ($this->getWords() as $word) { if (\mb_substr($word, 0, $length) === $prefix) { $out[] = $word; } } if (empty($out)) { return null; } if (1 === \count($out)) { return $out[0]; } return $out; } /** * Get definition of a word. */ public function getWordDefinition(): string { return '\b\w+'; } /** * Set list of words. * * @param array $words words * * @return array */ public function setWords(array $words) { $old = $this->_words; $this->_words = $words; return $old; } /** * Get list of words. */ public function getWords(): array { return $this->_words; } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamPathable.php���������������������������������������������������������������������0000644�����������������00000003744�15025056514�0012416 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Pathable. * * Interface for pathable input/output. */ interface StreamPathable extends IStream { /** * Get filename component of path. */ public function getBasename(): string; /** * Get directory name component of path. */ public function getDirname(): string; } ����������������������������Readline/Hoa/ConsoleOutput.php����������������������������������������������������������������������0000644�����������������00000012355�15025056514�0012343 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Output. * * This class represents the output of a program. Most of the time, this is * going to be STDOUT. */ class ConsoleOutput implements StreamOut { /** * Whether the multiplexer must be considered while writing on the output. */ protected $_considerMultiplexer = false; /** * Real output stream. */ protected $_output = null; /** * Wraps an `Hoa\Stream\IStream\Out` stream. */ public function __construct(StreamOut $output = null) { $this->_output = $output; return; } /** * Get the real output stream. */ public function getStream(): StreamOut { return $this->_output; } /** * Write n characters. */ public function write(string $string, int $length) { if (0 > $length) { throw new ConsoleException('Length must be greater than 0, given %d.', 0, $length); } $out = \substr($string, 0, $length); if (true === $this->isMultiplexerConsidered()) { if (true === Console::isTmuxRunning()) { $out = "\033Ptmux;". \str_replace("\033", "\033\033", $out). "\033\\"; } $length = \strlen($out); } if (null === $this->_output) { echo $out; } else { $this->_output->write($out, $length); } } /** * Write a string. */ public function writeString(string $string) { $string = (string) $string; return $this->write($string, \strlen($string)); } /** * Write a character. */ public function writeCharacter(string $character) { return $this->write((string) $character[0], 1); } /** * Write a boolean. */ public function writeBoolean(bool $boolean) { return $this->write(((bool) $boolean) ? '1' : '0', 1); } /** * Write an integer. */ public function writeInteger(int $integer) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer)); } /** * Write a float. */ public function writeFloat(float $float) { $float = (string) (float) $float; return $this->write($float, \strlen($float)); } /** * Write an array. */ public function writeArray(array $array) { $array = \var_export($array, true); return $this->write($array, \strlen($array)); } /** * Write a line. */ public function writeLine(string $line) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1); } ++$n; return $this->write(\substr($line, 0, $n), $n); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string) { return $this->write($string ?? '', \strlen($string ?? '')); } /** * Truncate a stream to a given length. */ public function truncate(int $size): bool { return false; } /** * Consider the multiplexer (if running) while writing on the output. */ public function considerMultiplexer(bool $consider): bool { $old = $this->_considerMultiplexer; $this->_considerMultiplexer = $consider; return $old; } /** * Check whether the multiplexer must be considered or not. */ public function isMultiplexerConsidered(): bool { return $this->_considerMultiplexer; } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/ConsoleCursor.php����������������������������������������������������������������������0000644�����������������00000050551�15025056514�0012320 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Cursor. * * Allow to manipulate the cursor. */ class ConsoleCursor { /** * Move the cursor. * Steps can be: * • u, up, ↑ : move to the previous line; * • U, UP : move to the first line; * • r, right, → : move to the next column; * • R, RIGHT : move to the last column; * • d, down, ↓ : move to the next line; * • D, DOWN : move to the last line; * • l, left, ← : move to the previous column; * • L, LEFT : move to the first column. * Steps can be concatened by a single space if $repeat is equal to 1. */ public static function move(string $steps, int $repeat = 1) { if (1 > $repeat) { return; } elseif (1 === $repeat) { $handle = \explode(' ', $steps); } else { $handle = \explode(' ', $steps, 1); } $tput = Console::getTput(); $output = Console::getOutput(); foreach ($handle as $step) { switch ($step) { case 'u': case 'up': case '↑': $output->writeAll( \str_replace( '%p1%d', $repeat, $tput->get('parm_up_cursor') ) ); break; case 'U': case 'UP': static::moveTo(null, 1); break; case 'r': case 'right': case '→': $output->writeAll( \str_replace( '%p1%d', $repeat, $tput->get('parm_right_cursor') ) ); break; case 'R': case 'RIGHT': static::moveTo(9999); break; case 'd': case 'down': case '↓': $output->writeAll( \str_replace( '%p1%d', $repeat, $tput->get('parm_down_cursor') ) ); break; case 'D': case 'DOWN': static::moveTo(null, 9999); break; case 'l': case 'left': case '←': $output->writeAll( \str_replace( '%p1%d', $repeat, $tput->get('parm_left_cursor') ) ); break; case 'L': case 'LEFT': static::moveTo(1); break; } } } /** * Move to the line X and the column Y. * If null, use the current coordinate. */ public static function moveTo(int $x = null, int $y = null) { if (null === $x || null === $y) { $position = static::getPosition(); if (null === $x) { $x = $position['x']; } if (null === $y) { $y = $position['y']; } } Console::getOutput()->writeAll( \str_replace( ['%i%p1%d', '%p2%d'], [$y, $x], Console::getTput()->get('cursor_address') ) ); } /** * Get current position (x and y) of the cursor. */ public static function getPosition(): array { $tput = Console::getTput(); $user7 = $tput->get('user7'); if (null === $user7) { return [ 'x' => 0, 'y' => 0, ]; } Console::getOutput()->writeAll($user7); $input = Console::getInput(); // Read $tput->get('user6'). $input->read(2); // skip \033 and [. $x = null; $y = null; $handle = &$y; while (true) { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$x; break; case 'R': break 2; default: $handle .= $char; } } return [ 'x' => (int) $x, 'y' => (int) $y, ]; } /** * Save current position. */ public static function save() { Console::getOutput()->writeAll( Console::getTput()->get('save_cursor') ); } /** * Restore cursor to the last saved position. */ public static function restore() { Console::getOutput()->writeAll( Console::getTput()->get('restore_cursor') ); } /** * Clear the screen. * Part can be: * • a, all, ↕ : clear entire screen and static::move(1, 1); * • u, up, ↑ : clear from cursor to beginning of the screen; * • r, right, → : clear from cursor to the end of the line; * • d, down, ↓ : clear from cursor to end of the screen; * • l, left, ← : clear from cursor to beginning of the screen; * • line, ↔ : clear all the line and static::move(1). * Parts can be concatenated by a single space. */ public static function clear(string $parts = 'all') { $tput = Console::getTput(); $output = Console::getOutput(); foreach (\explode(' ', $parts) as $part) { switch ($part) { case 'a': case 'all': case '↕': $output->writeAll($tput->get('clear_screen')); static::moveTo(1, 1); break; case 'u': case 'up': case '↑': $output->writeAll("\033[1J"); break; case 'r': case 'right': case '→': $output->writeAll($tput->get('clr_eol')); break; case 'd': case 'down': case '↓': $output->writeAll($tput->get('clr_eos')); break; case 'l': case 'left': case '←': $output->writeAll($tput->get('clr_bol')); break; case 'line': case '↔': $output->writeAll("\r".$tput->get('clr_eol')); break; } } } /** * Hide the cursor. */ public static function hide() { Console::getOutput()->writeAll( Console::getTput()->get('cursor_invisible') ); } /** * Show the cursor. */ public static function show() { Console::getOutput()->writeAll( Console::getTput()->get('cursor_visible') ); } /** * Colorize cursor. * Attributes can be: * • n, normal : normal; * • b, bold : bold; * • u, underlined : underlined; * • bl, blink : blink; * • i, inverse : inverse; * • !b, !bold : normal weight; * • !u, !underlined : not underlined; * • !bl, !blink : steady; * • !i, !inverse : positive; * • fg(color), foreground(color) : set foreground to “color”; * • bg(color), background(color) : set background to “color”. * “color” can be: * • default; * • black; * • red; * • green; * • yellow; * • blue; * • magenta; * • cyan; * • white; * • 0-256 (classic palette); * • #hexa. * Attributes can be concatenated by a single space. */ public static function colorize(string $attributes) { static $_rgbTo256 = null; if (null === $_rgbTo256) { $_rgbTo256 = [ '000000', '800000', '008000', '808000', '000080', '800080', '008080', 'c0c0c0', '808080', 'ff0000', '00ff00', 'ffff00', '0000ff', 'ff00ff', '00ffff', 'ffffff', '000000', '00005f', '000087', '0000af', '0000d7', '0000ff', '005f00', '005f5f', '005f87', '005faf', '005fd7', '005fff', '008700', '00875f', '008787', '0087af', '0087d7', '0087ff', '00af00', '00af5f', '00af87', '00afaf', '00afd7', '00afff', '00d700', '00d75f', '00d787', '00d7af', '00d7d7', '00d7ff', '00ff00', '00ff5f', '00ff87', '00ffaf', '00ffd7', '00ffff', '5f0000', '5f005f', '5f0087', '5f00af', '5f00d7', '5f00ff', '5f5f00', '5f5f5f', '5f5f87', '5f5faf', '5f5fd7', '5f5fff', '5f8700', '5f875f', '5f8787', '5f87af', '5f87d7', '5f87ff', '5faf00', '5faf5f', '5faf87', '5fafaf', '5fafd7', '5fafff', '5fd700', '5fd75f', '5fd787', '5fd7af', '5fd7d7', '5fd7ff', '5fff00', '5fff5f', '5fff87', '5fffaf', '5fffd7', '5fffff', '870000', '87005f', '870087', '8700af', '8700d7', '8700ff', '875f00', '875f5f', '875f87', '875faf', '875fd7', '875fff', '878700', '87875f', '878787', '8787af', '8787d7', '8787ff', '87af00', '87af5f', '87af87', '87afaf', '87afd7', '87afff', '87d700', '87d75f', '87d787', '87d7af', '87d7d7', '87d7ff', '87ff00', '87ff5f', '87ff87', '87ffaf', '87ffd7', '87ffff', 'af0000', 'af005f', 'af0087', 'af00af', 'af00d7', 'af00ff', 'af5f00', 'af5f5f', 'af5f87', 'af5faf', 'af5fd7', 'af5fff', 'af8700', 'af875f', 'af8787', 'af87af', 'af87d7', 'af87ff', 'afaf00', 'afaf5f', 'afaf87', 'afafaf', 'afafd7', 'afafff', 'afd700', 'afd75f', 'afd787', 'afd7af', 'afd7d7', 'afd7ff', 'afff00', 'afff5f', 'afff87', 'afffaf', 'afffd7', 'afffff', 'd70000', 'd7005f', 'd70087', 'd700af', 'd700d7', 'd700ff', 'd75f00', 'd75f5f', 'd75f87', 'd75faf', 'd75fd7', 'd75fff', 'd78700', 'd7875f', 'd78787', 'd787af', 'd787d7', 'd787ff', 'd7af00', 'd7af5f', 'd7af87', 'd7afaf', 'd7afd7', 'd7afff', 'd7d700', 'd7d75f', 'd7d787', 'd7d7af', 'd7d7d7', 'd7d7ff', 'd7ff00', 'd7ff5f', 'd7ff87', 'd7ffaf', 'd7ffd7', 'd7ffff', 'ff0000', 'ff005f', 'ff0087', 'ff00af', 'ff00d7', 'ff00ff', 'ff5f00', 'ff5f5f', 'ff5f87', 'ff5faf', 'ff5fd7', 'ff5fff', 'ff8700', 'ff875f', 'ff8787', 'ff87af', 'ff87d7', 'ff87ff', 'ffaf00', 'ffaf5f', 'ffaf87', 'ffafaf', 'ffafd7', 'ffafff', 'ffd700', 'ffd75f', 'ffd787', 'ffd7af', 'ffd7d7', 'ffd7ff', 'ffff00', 'ffff5f', 'ffff87', 'ffffaf', 'ffffd7', 'ffffff', '080808', '121212', '1c1c1c', '262626', '303030', '3a3a3a', '444444', '4e4e4e', '585858', '606060', '666666', '767676', '808080', '8a8a8a', '949494', '9e9e9e', 'a8a8a8', 'b2b2b2', 'bcbcbc', 'c6c6c6', 'd0d0d0', 'dadada', 'e4e4e4', 'eeeeee', ]; } $tput = Console::getTput(); if (1 >= $tput->count('max_colors')) { return; } $handle = []; foreach (\explode(' ', $attributes) as $attribute) { switch ($attribute) { case 'n': case 'normal': $handle[] = 0; break; case 'b': case 'bold': $handle[] = 1; break; case 'u': case 'underlined': $handle[] = 4; break; case 'bl': case 'blink': $handle[] = 5; break; case 'i': case 'inverse': $handle[] = 7; break; case '!b': case '!bold': $handle[] = 22; break; case '!u': case '!underlined': $handle[] = 24; break; case '!bl': case '!blink': $handle[] = 25; break; case '!i': case '!inverse': $handle[] = 27; break; default: if (0 === \preg_match('#^([^\(]+)\(([^\)]+)\)$#', $attribute, $m)) { break; } $shift = 0; switch ($m[1]) { case 'fg': case 'foreground': $shift = 0; break; case 'bg': case 'background': $shift = 10; break; default: break 2; } $_handle = 0; $_keyword = true; switch ($m[2]) { case 'black': $_handle = 30; break; case 'red': $_handle = 31; break; case 'green': $_handle = 32; break; case 'yellow': $_handle = 33; break; case 'blue': $_handle = 34; break; case 'magenta': $_handle = 35; break; case 'cyan': $_handle = 36; break; case 'white': $_handle = 37; break; case 'default': $_handle = 39; break; default: $_keyword = false; if (256 <= $tput->count('max_colors') && '#' === $m[2][0]) { $rgb = \hexdec(\substr($m[2], 1)); $r = ($rgb >> 16) & 255; $g = ($rgb >> 8) & 255; $b = $rgb & 255; $distance = null; foreach ($_rgbTo256 as $i => $_rgb) { $_rgb = \hexdec($_rgb); $_r = ($_rgb >> 16) & 255; $_g = ($_rgb >> 8) & 255; $_b = $_rgb & 255; $d = \sqrt( ($_r - $r) ** 2 + ($_g - $g) ** 2 + ($_b - $b) ** 2 ); if (null === $distance || $d <= $distance) { $distance = $d; $_handle = $i; } } } else { $_handle = (int) ($m[2]); } } if (true === $_keyword) { $handle[] = $_handle + $shift; } else { $handle[] = (38 + $shift).';5;'.$_handle; } } } Console::getOutput()->writeAll("\033[".\implode(';', $handle).'m'); return; } /** * Change color number to a specific RGB color. */ public static function changeColor(int $fromCode, int $toColor) { $tput = Console::getTput(); if (true !== $tput->has('can_change')) { return; } $r = ($toColor >> 16) & 255; $g = ($toColor >> 8) & 255; $b = $toColor & 255; Console::getOutput()->writeAll( \str_replace( [ '%p1%d', 'rgb:', '%p2%{255}%*%{1000}%/%2.2X/', '%p3%{255}%*%{1000}%/%2.2X/', '%p4%{255}%*%{1000}%/%2.2X', ], [ $fromCode, '', \sprintf('%02x', $r), \sprintf('%02x', $g), \sprintf('%02x', $b), ], $tput->get('initialize_color') ) ); return; } /** * Set cursor style. * Style can be: * • b, block, ▋: block; * • u, underline, _: underline; * • v, vertical, |: vertical. */ public static function setStyle(string $style, bool $blink = true) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } switch ($style) { case 'u': case 'underline': case '_': $_style = 2; break; case 'v': case 'vertical': case '|': $_style = 5; break; case 'b': case 'block': case '▋': default: $_style = 1; break; } if (false === $blink) { ++$_style; } // Not sure what tput entry we can use here… Console::getOutput()->writeAll("\033[".$_style.' q'); return; } /** * Make a stupid “bip”. */ public static function bip() { Console::getOutput()->writeAll( Console::getTput()->get('bell') ); } } /* * Advanced interaction. */ Console::advancedInteraction(); �������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Protocol.php���������������������������������������������������������������������������0000644�����������������00000014674�15025056514�0011327 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Root of the `hoa://` protocol. */ class Protocol extends ProtocolNode { /** * No resolution value. * * @const string */ const NO_RESOLUTION = '/hoa/flatland'; /** * Singleton. */ private static $_instance = null; /** * Cache of resolver. */ private static $_cache = []; /** * Initialize the protocol. */ public function __construct() { $this->initialize(); return; } /** * Singleton. * To use the `hoa://` protocol shared by everyone. */ public static function getInstance(): self { if (null === static::$_instance) { static::$_instance = new self(); } return static::$_instance; } /** * Initialize the protocol. */ protected function initialize() { $root = \dirname(__DIR__, 3); $argv0 = \realpath($_SERVER['argv'][0]); $cwd = 'cli' === \PHP_SAPI ? false !== $argv0 ? \dirname($argv0) : '' : \getcwd(); $this[] = new ProtocolNode( 'Application', $cwd.\DIRECTORY_SEPARATOR, [ new ProtocolNode('Public', 'Public'.\DIRECTORY_SEPARATOR), ] ); $this[] = new ProtocolNode( 'Data', \dirname($cwd).\DIRECTORY_SEPARATOR, [ new ProtocolNode( 'Etc', 'Etc'.\DIRECTORY_SEPARATOR, [ new ProtocolNode('Configuration', 'Configuration'.\DIRECTORY_SEPARATOR), new ProtocolNode('Locale', 'Locale'.\DIRECTORY_SEPARATOR), ] ), new ProtocolNode('Lost+found', 'Lost+found'.\DIRECTORY_SEPARATOR), new ProtocolNode('Temporary', 'Temporary'.\DIRECTORY_SEPARATOR), new ProtocolNode( 'Variable', 'Variable'.\DIRECTORY_SEPARATOR, [ new ProtocolNode('Cache', 'Cache'.\DIRECTORY_SEPARATOR), new ProtocolNode('Database', 'Database'.\DIRECTORY_SEPARATOR), new ProtocolNode('Log', 'Log'.\DIRECTORY_SEPARATOR), new ProtocolNode('Private', 'Private'.\DIRECTORY_SEPARATOR), new ProtocolNode('Run', 'Run'.\DIRECTORY_SEPARATOR), new ProtocolNode('Test', 'Test'.\DIRECTORY_SEPARATOR), ] ), ] ); $this[] = new ProtocolNodeLibrary( 'Library', $root.\DIRECTORY_SEPARATOR.'Hoathis'.\DIRECTORY_SEPARATOR.';'. $root.\DIRECTORY_SEPARATOR.'Hoa'.\DIRECTORY_SEPARATOR ); } /** * Resolve (unfold) an `hoa://` path to its real resource. * * If `$exists` is set to `true`, try to find the first that exists, * otherwise returns the first solution. If `$unfold` is set to `true`, * it returns all the paths. */ public function resolve(string $path, bool $exists = true, bool $unfold = false) { if (\substr($path, 0, 6) !== 'hoa://') { if (true === \is_dir($path)) { $path = \rtrim($path, '/\\'); if ('' === $path) { $path = '/'; } } return $path; } if (isset(self::$_cache[$path])) { $handle = self::$_cache[$path]; } else { $out = $this->_resolve($path, $handle); // Not a path but a resource. if (!\is_array($handle)) { return $out; } $handle = \array_values(\array_unique($handle, \SORT_REGULAR)); foreach ($handle as &$entry) { if (true === \is_dir($entry)) { $entry = \rtrim($entry, '/\\'); if ('' === $entry) { $entry = '/'; } } } self::$_cache[$path] = $handle; } if (true === $unfold) { if (true !== $exists) { return $handle; } $out = []; foreach ($handle as $solution) { if (\file_exists($solution)) { $out[] = $solution; } } return $out; } if (true !== $exists) { return $handle[0]; } foreach ($handle as $solution) { if (\file_exists($solution)) { return $solution; } } return static::NO_RESOLUTION; } /** * Clear the cache. */ public static function clearCache() { self::$_cache = []; } } ��������������������������������������������������������������������Readline/Hoa/ConsoleTput.php������������������������������������������������������������������������0000644�����������������00000051636�15025056514�0012004 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Tput. * * Query terminfo database. * Resources: * • http://man.cx/terminfo(5), * • http://pubs.opengroup.org/onlinepubs/7908799/xcurses/terminfo.html, */ class ConsoleTput { /** * Booleans. */ protected static $_booleans = [ 'auto_left_margin', 'auto_right_margin', 'no_esc_ctlc', 'ceol_standout_glitch', 'eat_newline_glitch', 'erase_overstrike', 'generic_type', 'hard_copy', 'meta_key', // originally has_meta_key 'status_line', // originally has_status_line 'insert_null_glitch', 'memory_above', 'memory_below', 'move_insert_mode', 'move_standout_mode', 'over_strike', 'status_line_esc_ok', 'dest_tabs_magic_smso', 'tilde_glitch', 'transparent_underline', 'xon_xoff', 'needs_xon_xoff', 'prtr_silent', 'hard_cursor', 'non_rev_rmcup', 'no_pad_char', 'non_dest_scroll_region', 'can_change', 'back_color_erase', 'hue_lightness_saturation', 'col_addr_glitch', 'cr_cancels_micro_mode', 'print_wheel', // originally has_print_wheel 'row_addr_glitch', 'semi_auto_right_margin', 'cpi_changes_res', 'lpi_changes_res', // #ifdef __INTERNAL_CAPS_VISIBLE 'backspaces_with_bs', 'crt_no_scrolling', 'no_correctly_working_cr', 'gnu_meta_key', // originally gnu_has_meta_key 'linefeed_is_newline', 'hardware_tabs', // originally has_hardware_tabs 'return_does_clr_eol', ]; /** * Numbers. */ protected static $_numbers = [ 'columns', 'init_tabs', 'lines', 'lines_of_memory', 'magic_cookie_glitch', 'padding_baud_rate', 'virtual_terminal', 'width_status_line', 'num_labels', 'label_height', 'label_width', 'max_attributes', 'maximum_windows', 'max_colors', 'max_pairs', 'no_color_video', 'buffer_capacity', 'dot_vert_spacing', 'dot_horz_spacing', 'max_micro_address', 'max_micro_jump', 'micro_col_size', 'micro_line_size', 'number_of_pins', 'output_res_char', 'output_res_line', 'output_res_horz_inch', 'output_res_vert_inch', 'print_rate', 'wide_char_size', 'buttons', 'bit_image_entwining', 'bit_image_type', // #ifdef __INTERNAL_CAPS_VISIBLE 'magic_cookie_glitch_ul', 'carriage_return_delay', 'new_line_delay', 'backspace_delay', 'horizontal_tab_delay', 'number_of_function_keys', ]; /** * Strings. */ protected static $_strings = [ 'back_tab', 'bell', 'carriage_return', 'change_scroll_region', 'clear_all_tabs', 'clear_screen', 'clr_eol', 'clr_eos', 'column_address', 'command_character', 'cursor_address', 'cursor_down', 'cursor_home', 'cursor_invisible', 'cursor_left', 'cursor_mem_address', 'cursor_normal', 'cursor_right', 'cursor_to_ll', 'cursor_up', 'cursor_visible', 'delete_character', 'delete_line', 'dis_status_line', 'down_half_line', 'enter_alt_charset_mode', 'enter_blink_mode', 'enter_bold_mode', 'enter_ca_mode', 'enter_delete_mode', 'enter_dim_mode', 'enter_insert_mode', 'enter_secure_mode', 'enter_protected_mode', 'enter_reverse_mode', 'enter_standout_mode', 'enter_underline_mode', 'erase_chars', 'exit_alt_charset_mode', 'exit_attribute_mode', 'exit_ca_mode', 'exit_delete_mode', 'exit_insert_mode', 'exit_standout_mode', 'exit_underline_mode', 'flash_screen', 'form_feed', 'from_status_line', 'init_1string', 'init_2string', 'init_3string', 'init_file', 'insert_character', 'insert_line', 'insert_padding', 'key_backspace', 'key_catab', 'key_clear', 'key_ctab', 'key_dc', 'key_dl', 'key_down', 'key_eic', 'key_eol', 'key_eos', 'key_f0', 'key_f1', 'key_f10', 'key_f2', 'key_f3', 'key_f4', 'key_f5', 'key_f6', 'key_f7', 'key_f8', 'key_f9', 'key_home', 'key_ic', 'key_il', 'key_left', 'key_ll', 'key_npage', 'key_ppage', 'key_right', 'key_sf', 'key_sr', 'key_stab', 'key_up', 'keypad_local', 'keypad_xmit', 'lab_f0', 'lab_f1', 'lab_f10', 'lab_f2', 'lab_f3', 'lab_f4', 'lab_f5', 'lab_f6', 'lab_f7', 'lab_f8', 'lab_f9', 'meta_off', 'meta_on', 'newline', 'pad_char', 'parm_dch', 'parm_delete_line', 'parm_down_cursor', 'parm_ich', 'parm_index', 'parm_insert_line', 'parm_left_cursor', 'parm_right_cursor', 'parm_rindex', 'parm_up_cursor', 'pkey_key', 'pkey_local', 'pkey_xmit', 'print_screen', 'prtr_off', 'prtr_on', 'repeat_char', 'reset_1string', 'reset_2string', 'reset_3string', 'reset_file', 'restore_cursor', 'row_address', 'save_cursor', 'scroll_forward', 'scroll_reverse', 'set_attributes', 'set_tab', 'set_window', 'tab', 'to_status_line', 'underline_char', 'up_half_line', 'init_prog', 'key_a1', 'key_a3', 'key_b2', 'key_c1', 'key_c3', 'prtr_non', 'char_padding', 'acs_chars', 'plab_norm', 'key_btab', 'enter_xon_mode', 'exit_xon_mode', 'enter_am_mode', 'exit_am_mode', 'xon_character', 'xoff_character', 'ena_acs', 'label_on', 'label_off', 'key_beg', 'key_cancel', 'key_close', 'key_command', 'key_copy', 'key_create', 'key_end', 'key_enter', 'key_exit', 'key_find', 'key_help', 'key_mark', 'key_message', 'key_move', 'key_next', 'key_open', 'key_options', 'key_previous', 'key_print', 'key_redo', 'key_reference', 'key_refresh', 'key_replace', 'key_restart', 'key_resume', 'key_save', 'key_suspend', 'key_undo', 'key_sbeg', 'key_scancel', 'key_scommand', 'key_scopy', 'key_screate', 'key_sdc', 'key_sdl', 'key_select', 'key_send', 'key_seol', 'key_sexit', 'key_sfind', 'key_shelp', 'key_shome', 'key_sic', 'key_sleft', 'key_smessage', 'key_smove', 'key_snext', 'key_soptions', 'key_sprevious', 'key_sprint', 'key_sredo', 'key_sreplace', 'key_sright', 'key_srsume', 'key_ssave', 'key_ssuspend', 'key_sundo', 'req_for_input', 'key_f11', 'key_f12', 'key_f13', 'key_f14', 'key_f15', 'key_f16', 'key_f17', 'key_f18', 'key_f19', 'key_f20', 'key_f21', 'key_f22', 'key_f23', 'key_f24', 'key_f25', 'key_f26', 'key_f27', 'key_f28', 'key_f29', 'key_f30', 'key_f31', 'key_f32', 'key_f33', 'key_f34', 'key_f35', 'key_f36', 'key_f37', 'key_f38', 'key_f39', 'key_f40', 'key_f41', 'key_f42', 'key_f43', 'key_f44', 'key_f45', 'key_f46', 'key_f47', 'key_f48', 'key_f49', 'key_f50', 'key_f51', 'key_f52', 'key_f53', 'key_f54', 'key_f55', 'key_f56', 'key_f57', 'key_f58', 'key_f59', 'key_f60', 'key_f61', 'key_f62', 'key_f63', 'clr_bol', 'clear_margins', 'set_left_margin', 'set_right_margin', 'label_format', 'set_clock', 'display_clock', 'remove_clock', 'create_window', 'goto_window', 'hangup', 'dial_phone', 'quick_dial', 'tone', 'pulse', 'flash_hook', 'fixed_pause', 'wait_tone', 'user0', 'user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7', 'user8', 'user9', 'orig_pair', 'orig_colors', 'initialize_color', 'initialize_pair', 'set_color_pair', 'set_foreground', 'set_background', 'change_char_pitch', 'change_line_pitch', 'change_res_horz', 'change_res_vert', 'define_char', 'enter_doublewide_mode', 'enter_draft_quality', 'enter_italics_mode', 'enter_leftward_mode', 'enter_micro_mode', 'enter_near_letter_quality', 'enter_normal_quality', 'enter_shadow_mode', 'enter_subscript_mode', 'enter_superscript_mode', 'enter_upward_mode', 'exit_doublewide_mode', 'exit_italics_mode', 'exit_leftward_mode', 'exit_micro_mode', 'exit_shadow_mode', 'exit_subscript_mode', 'exit_superscript_mode', 'exit_upward_mode', 'micro_column_address', 'micro_down', 'micro_left', 'micro_right', 'micro_row_address', 'micro_up', 'order_of_pins', 'parm_down_micro', 'parm_left_micro', 'parm_right_micro', 'parm_up_micro', 'select_char_set', 'set_bottom_margin', 'set_bottom_margin_parm', 'set_left_margin_parm', 'set_right_margin_parm', 'set_top_margin', 'set_top_margin_parm', 'start_bit_image', 'start_char_set_def', 'stop_bit_image', 'stop_char_set_def', 'subscript_characters', 'superscript_characters', 'these_cause_cr', 'zero_motion', 'char_set_names', 'key_mouse', 'mouse_info', 'req_mouse_pos', 'get_mouse', 'set_a_foreground', 'set_a_background', 'pkey_plab', 'device_type', 'code_set_init', 'set0_des_seq', 'set1_des_seq', 'set2_des_seq', 'set3_des_seq', 'set_lr_margin', 'set_tb_margin', 'bit_image_repeat', 'bit_image_newline', 'bit_image_carriage_return', 'color_names', 'define_bit_image_region', 'end_bit_image_region', 'set_color_band', 'set_page_length', 'display_pc_char', 'enter_pc_charset_mode', 'exit_pc_charset_mode', 'enter_scancode_mode', 'exit_scancode_mode', 'pc_term_options', 'scancode_escape', 'alt_scancode_esc', 'enter_horizontal_hl_mode', 'enter_left_hl_mode', 'enter_low_hl_mode', 'enter_right_hl_mode', 'enter_top_hl_mode', 'enter_vertical_hl_mode', 'set_a_attributes', 'set_pglen_inch', // #ifdef __INTERNAL_CAPS_VISIBLE 'termcap_init2', 'termcap_reset', 'linefeed_if_not_lf', 'backspace_if_not_bs', 'other_non_function_keys', 'arrow_key_map', 'acs_ulcorner', 'acs_llcorner', 'acs_urcorner', 'acs_lrcorner', 'acs_ltee', 'acs_rtee', 'acs_btee', 'acs_ttee', 'acs_hline', 'acs_vline', 'acs_plus', 'memory_lock', 'memory_unlock', 'box_chars_1', ]; /** * Computed informations. */ protected $_informations = []; /** * Set stream and parse. */ public function __construct($terminfo = null) { if (null === $terminfo) { $terminfo = static::getTerminfo(); } $this->parse($terminfo); return; } /** * Parse. */ protected function parse(string $terminfo): array { if (!\file_exists($terminfo)) { throw new ConsoleException('Terminfo file %s does not exist.', 0, $terminfo); } $data = \file_get_contents($terminfo); $length = \strlen($data); $out = ['file' => $terminfo]; $headers = [ 'data_size' => $length, 'header_size' => 12, 'magic_number' => (\ord($data[1]) << 8) | \ord($data[0]), 'names_size' => (\ord($data[3]) << 8) | \ord($data[2]), 'bool_count' => (\ord($data[5]) << 8) | \ord($data[4]), 'number_count' => (\ord($data[7]) << 8) | \ord($data[6]), 'string_count' => (\ord($data[9]) << 8) | \ord($data[8]), 'string_table_size' => (\ord($data[11]) << 8) | \ord($data[10]), ]; $out['headers'] = $headers; // Names. $i = $headers['header_size']; $nameAndDescription = \explode('|', \substr($data, $i, $headers['names_size'] - 1)); $out['name'] = $nameAndDescription[0]; $out['description'] = $nameAndDescription[1]; // Booleans. $i += $headers['names_size']; $booleans = []; $booleanNames = &static::$_booleans; for ( $e = 0, $max = $i + $headers['bool_count']; $i < $max; ++$e, ++$i ) { $booleans[$booleanNames[$e]] = 1 === \ord($data[$i]); } $out['booleans'] = $booleans; // Numbers. if (1 === ($i % 2)) { ++$i; } $numbers = []; $numberNames = &static::$_numbers; for ( $e = 0, $max = $i + $headers['number_count'] * 2; $i < $max; ++$e, $i += 2 ) { $name = $numberNames[$e]; $data_i0 = \ord($data[$i]); $data_i1 = \ord($data[$i + 1]); if ($data_i1 === 255 && $data_i0 === 255) { $numbers[$name] = -1; } else { $numbers[$name] = ($data_i1 << 8) | $data_i0; } } $out['numbers'] = $numbers; // Strings. $strings = []; $stringNames = &static::$_strings; $ii = $i + $headers['string_count'] * 2; for ( $e = 0, $max = $ii; $i < $max; ++$e, $i += 2 ) { $name = $stringNames[$e]; $data_i0 = \ord($data[$i]); $data_i1 = \ord($data[$i + 1]); if ($data_i1 === 255 && $data_i0 === 255) { continue; } $a = ($data_i1 << 8) | $data_i0; $strings[$name] = $a; if (65534 === $a) { continue; } $b = $ii + $a; $c = $b; while ($c < $length && \ord($data[$c])) { $c++; } $value = \substr($data, $b, $c - $b); $strings[$name] = false !== $value ? $value : null; } $out['strings'] = $strings; return $this->_informations = $out; } /** * Get all informations. */ public function getInformations(): array { return $this->_informations; } /** * Get a boolean value. */ public function has(string $boolean): bool { if (!isset($this->_informations['booleans'][$boolean])) { return false; } return $this->_informations['booleans'][$boolean]; } /** * Get a number value. */ public function count(string $number): int { if (!isset($this->_informations['numbers'][$number])) { return 0; } return $this->_informations['numbers'][$number]; } /** * Get a string value. */ public function get(string $string) { if (!isset($this->_informations['strings'][$string])) { return null; } return $this->_informations['strings'][$string]; } /** * Get current term profile. */ public static function getTerm(): string { return isset($_SERVER['TERM']) && !empty($_SERVER['TERM']) ? $_SERVER['TERM'] : (\defined('PHP_WINDOWS_VERSION_PLATFORM') ? 'windows-ansi' : 'xterm'); } /** * Get pathname to the current terminfo. */ public static function getTerminfo($term = null): string { $paths = []; if (isset($_SERVER['TERMINFO'])) { $paths[] = $_SERVER['TERMINFO']; } if (isset($_SERVER['HOME'])) { $paths[] = $_SERVER['HOME'].\DIRECTORY_SEPARATOR.'.terminfo'; } if (isset($_SERVER['TERMINFO_DIRS'])) { foreach (\explode(':', $_SERVER['TERMINFO_DIRS']) as $path) { $paths[] = $path; } } $paths[] = '/usr/share/terminfo'; $paths[] = '/usr/share/lib/terminfo'; $paths[] = '/lib/terminfo'; $paths[] = '/usr/lib/terminfo'; $paths[] = '/usr/local/share/terminfo'; $paths[] = '/usr/local/share/lib/terminfo'; $paths[] = '/usr/local/lib/terminfo'; $paths[] = '/usr/local/ncurses/lib/terminfo'; $paths[] = 'hoa://Library/Terminfo'; $term = $term ?: static::getTerm(); $fileHexa = \dechex(\ord($term[0])).\DIRECTORY_SEPARATOR.$term; $fileAlpha = $term[0].\DIRECTORY_SEPARATOR.$term; $pathname = null; foreach ($paths as $path) { if (\file_exists($_ = $path.\DIRECTORY_SEPARATOR.$fileHexa) || \file_exists($_ = $path.\DIRECTORY_SEPARATOR.$fileAlpha)) { $pathname = $_; break; } } if (null === $pathname && 'xterm' !== $term) { return static::getTerminfo('xterm'); } return $pathname ?? ''; } /** * Check whether all required terminfo capabilities are defined. */ public static function isSupported(): bool { if (static::getTerminfo() === '') { return false; } $requiredVars = [ 'clear_screen', 'clr_bol', 'clr_eol', 'clr_eos', 'initialize_color', 'parm_down_cursor', 'parm_index', 'parm_left_cursor', 'parm_right_cursor', 'parm_rindex', 'parm_up_cursor', 'user6', 'user7', ]; $tput = new self(); foreach ($requiredVars as $var) { if ($tput->get($var) === null) { return false; } } return true; } } ��������������������������������������������������������������������������������������������������Readline/Hoa/ExceptionIdle.php����������������������������������������������������������������������0000644�����������������00000016106�15025056514�0012252 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * `Hoa\Exception\Idle` is the mother exception class of libraries. The only * difference between `Hoa\Exception\Idle` and its direct children * `Hoa\Exception` is that the latter fires events after beeing constructed. */ class ExceptionIdle extends \Exception { /** * Delay processing on arguments. */ protected $_tmpArguments = null; /** * List of arguments to format message. */ protected $_arguments = null; /** * Backtrace. */ protected $_trace = null; /** * Previous exception if any. */ protected $_previous = null; /** * Original exception message. */ protected $_rawMessage = null; /** * Allocates a new exception. * * An exception is built with a formatted message, a code (an ID) and an * array that contains the list of formatted strings for the message. If * chaining, we can add a previous exception. */ public function __construct( string $message, int $code = 0, $arguments = [], \Exception $previous = null ) { $this->_tmpArguments = $arguments; parent::__construct($message, $code, $previous); $this->_rawMessage = $message; $this->message = @\vsprintf($message, $this->getArguments()); return; } /** * Returns the backtrace. * * Do not use `Exception::getTrace` any more. */ public function getBacktrace() { if (null === $this->_trace) { $this->_trace = $this->getTrace(); } return $this->_trace; } /** * Returns the previous exception if any. * * Do not use `Exception::getPrevious` any more. */ public function getPreviousThrow() { if (null === $this->_previous) { $this->_previous = $this->getPrevious(); } return $this->_previous; } /** * Returns the arguments of the message. */ public function getArguments() { if (null === $this->_arguments) { $arguments = $this->_tmpArguments; if (!\is_array($arguments)) { $arguments = [$arguments]; } foreach ($arguments as &$value) { if (null === $value) { $value = '(null)'; } } $this->_arguments = $arguments; unset($this->_tmpArguments); } return $this->_arguments; } /** * Returns the raw message. */ public function getRawMessage(): string { return $this->_rawMessage; } /** * Returns the message already formatted. */ public function getFormattedMessage(): string { return $this->getMessage(); } /** * Returns the source of the exception (class, method, function, main etc.). */ public function getFrom(): string { $trace = $this->getBacktrace(); $from = '{main}'; if (!empty($trace)) { $t = $trace[0]; $from = ''; if (isset($t['class'])) { $from .= $t['class'].'::'; } if (isset($t['function'])) { $from .= $t['function'].'()'; } } return $from; } /** * Raises an exception as a string. */ public function raise(bool $includePrevious = false): string { $message = $this->getFormattedMessage(); $trace = $this->getBacktrace(); $file = '/dev/null'; $line = -1; $pre = $this->getFrom(); if (!empty($trace)) { $file = $trace['file'] ?? null; $line = $trace['line'] ?? null; } $pre .= ': '; try { $out = $pre.'('.$this->getCode().') '.$message."\n". 'in '.$this->getFile().' at line '. $this->getLine().'.'; } catch (\Exception $e) { $out = $pre.'('.$this->getCode().') '.$message."\n". 'in '.$file.' around line '.$line.'.'; } if (true === $includePrevious && null !== $previous = $this->getPreviousThrow()) { $out .= "\n\n".' ⬇'."\n\n". 'Nested exception ('.\get_class($previous).'):'."\n". ($previous instanceof self ? $previous->raise(true) : $previous->getMessage()); } return $out; } /** * Catches uncaught exception (only `Hoa\Exception\Idle` and children). */ public static function uncaught(\Throwable $exception) { if (!($exception instanceof self)) { throw $exception; } while (0 < \ob_get_level()) { \ob_end_flush(); } echo 'Uncaught exception ('.\get_class($exception).'):'."\n". $exception->raise(true); } /** * String representation of object. */ public function __toString(): string { return $this->raise(); } /** * Enables uncaught exception handler. * * This is restricted to Hoa's exceptions only. */ public static function enableUncaughtHandler(bool $enable = true) { if (false === $enable) { return \restore_exception_handler(); } return \set_exception_handler(function ($exception) { return self::uncaught($exception); }); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamTouchable.php��������������������������������������������������������������������0000644�����������������00000006141�15025056514�0012576 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Touchable. * * Interface for touchable input/output. */ interface StreamTouchable extends IStream { /** * Overwrite file if already exists. */ const OVERWRITE = true; /** * Do not overwrite file if already exists. */ const DO_NOT_OVERWRITE = false; /** * Make directory if does not exist. */ const MAKE_DIRECTORY = true; /** * Do not make directory if does not exist. */ const DO_NOT_MAKE_DIRECTORY = false; /** * Set access and modification time of file. */ public function touch(int $time = -1, int $atime = -1): bool; /** * Copy file. * Return the destination file path if succeed, false otherwise. */ public function copy(string $to, bool $force = self::DO_NOT_OVERWRITE): bool; /** * Move a file. */ public function move( string $name, bool $force = self::DO_NOT_OVERWRITE, bool $mkdir = self::DO_NOT_MAKE_DIRECTORY ): bool; /** * Delete a file. */ public function delete(): bool; /** * Change file group. */ public function changeGroup($group): bool; /** * Change file mode. */ public function changeMode(int $mode): bool; /** * Change file owner. */ public function changeOwner($user): bool; /** * Change the current umask. */ public static function umask(int $umask = null): int; } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/AutocompleterPath.php������������������������������������������������������������������0000644�����������������00000011655�15025056514�0013162 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Readline\Autocompleter\Path. * * Path autocompleter. */ class AutocompleterPath implements Autocompleter { /** * Root is the current working directory. */ const PWD = null; /** * Root. */ protected $_root = null; /** * Iterator factory. Please, see the self::setIteratorFactory method. */ protected $_iteratorFactory = null; /** * Constructor. */ public function __construct( string $root = null, \Closure $iteratorFactory = null ) { if (null === $root) { $root = static::PWD; } if (null !== $root) { $this->setRoot($root); } if (null !== $iteratorFactory) { $this->setIteratorFactory($iteratorFactory); } } /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. */ public function complete(&$prefix) { $root = $this->getRoot(); if (static::PWD === $root) { $root = \getcwd(); } $path = $root.\DIRECTORY_SEPARATOR.$prefix; if (!\is_dir($path)) { $path = \dirname($path).\DIRECTORY_SEPARATOR; $prefix = \basename($prefix); } else { $prefix = null; } $iteratorFactory = $this->getIteratorFactory() ?: static::getDefaultIteratorFactory(); try { $iterator = $iteratorFactory($path); $out = []; $length = \mb_strlen($prefix); foreach ($iterator as $fileinfo) { $filename = $fileinfo->getFilename(); if (null === $prefix || (\mb_substr($filename, 0, $length) === $prefix)) { if ($fileinfo->isDir()) { $out[] = $filename.'/'; } else { $out[] = $filename; } } } } catch (\Exception $e) { return null; } $count = \count($out); if (1 === $count) { return $out[0]; } if (0 === $count) { return null; } return $out; } /** * Get definition of a word. */ public function getWordDefinition(): string { return '/?[\w\d\\_\-\.]+(/[\w\d\\_\-\.]*)*'; } /** * Set root. */ public function setRoot(string $root) { $old = $this->_root; $this->_root = $root; return $old; } /** * Get root. */ public function getRoot() { return $this->_root; } /** * Set iterator factory (a finder). */ public function setIteratorFactory(\Closure $iteratorFactory) { $old = $this->_iteratorFactory; $this->_iteratorFactory = $iteratorFactory; return $old; } /** * Get iterator factory. */ public function getIteratorFactory() { return $this->_iteratorFactory; } /** * Get default iterator factory (based on \DirectoryIterator). */ public static function getDefaultIteratorFactory() { return function ($path) { return new \DirectoryIterator($path); }; } } �����������������������������������������������������������������������������������Readline/Hoa/Exception.php��������������������������������������������������������������������������0000644�����������������00000005237�15025056514�0011457 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Each exception must extend `Hoa\Exception\Exception`. */ class Exception extends ExceptionIdle implements EventSource { /** * Allocates a new exception. * * An exception is built with a formatted message, a code (an ID), and an * array that contains the list of formatted string for the message. If * chaining, a previous exception can be added. */ public function __construct( string $message, int $code = 0, $arguments = [], \Throwable $previous = null ) { parent::__construct($message, $code, $arguments, $previous); if (false === Event::eventExists('hoa://Event/Exception')) { Event::register('hoa://Event/Exception', __CLASS__); } $this->send(); return; } /** * Sends the exception on `hoa://Event/Exception`. */ public function send() { Event::notify( 'hoa://Event/Exception', $this, new EventBucket($this) ); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileLink.php���������������������������������������������������������������������������0000644�����������������00000010277�15025056514�0011216 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Link. * * Link handler. */ class FileLink extends File { /** * Open a link. */ public function __construct( string $streamName, string $mode, string $context = null, bool $wait = false ) { if (!\is_link($streamName)) { throw new FileException('File %s is not a link.', 0, $streamName); } parent::__construct($streamName, $mode, $context, $wait); return; } /** * Get informations about a link. */ public function getStatistic(): array { return \lstat($this->getStreamName()); } /** * Change file group. */ public function changeGroup($group): bool { return \lchgrp($this->getStreamName(), $group); } /** * Change file owner. */ public function changeOwner($user): bool { return \lchown($this->getStreamName(), $user); } /** * Get file permissions. */ public function getPermissions(): int { return 41453; // i.e. lrwxr-xr-x } /** * Get the target of a symbolic link. */ public function getTarget(): FileGeneric { $target = \dirname($this->getStreamName()).\DIRECTORY_SEPARATOR. $this->getTargetName(); $context = null !== $this->getStreamContext() ? $this->getStreamContext()->getCurrentId() : null; if (true === \is_link($target)) { return new FileLinkReadWrite( $target, File::MODE_APPEND_READ_WRITE, $context ); } elseif (true === \is_file($target)) { return new FileReadWrite( $target, File::MODE_APPEND_READ_WRITE, $context ); } elseif (true === \is_dir($target)) { return new FileDirectory( $target, File::MODE_READ, $context ); } throw new FileException('Cannot find an appropriated object that matches with '.'path %s when defining it.', 1, $target); } /** * Get the target name of a symbolic link. */ public function getTargetName(): string { return \readlink($this->getStreamName()); } /** * Create a link. */ public static function create(string $name, string $target): bool { if (false !== \linkinfo($name)) { return true; } return \symlink($target, $name); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Autocompleter.php����������������������������������������������������������������������0000644�����������������00000004124�15025056514�0012336 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Console\Readline\Autocompleter. * * Interface for all auto-completers. */ interface Autocompleter { /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. */ public function complete(&$prefix); /** * Get definition of a word. * Example: \b\w+\b. PCRE delimiters and options must not be provided. */ public function getWordDefinition(): string; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/Xcallable.php��������������������������������������������������������������������������0000644�����������������00000016270�15025056514�0011407 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Build a callable object, i.e. `function`, `class::method`, `object->method` or * closure. They all have the same behaviour. This callable is an extension of * native PHP callable (aka callback) to integrate Hoa's structures. */ class Xcallable { /** * Callback with the PHP format. */ protected $_callback = null; /** * Callable hash. */ protected $_hash = null; /** * Allocates a xcallable based on a callback. * * Accepted forms: * * `'function'`, * * `'class::method'`, * * `'class', 'method'`, * * `$object, 'method'`, * * `$object, ''`, * * `function (…) { … }`, * * `['class', 'method']`, * * `[$object, 'method']`. * * # Examples * * ```php * $toUpper = new Hoa\Consistency\Xcallable('strtoupper'); * assert('FOO' === $toUpper('foo')); * ``` * * # Exceptions * * A `Hoa\Consistency\Exception` exception is thrown if the callback form * is invalid. * * ```php,must_throw(Hoa\Consistency\Exception) * new Hoa\Consistency\Xcallable('Foo:'); * ``` */ public function __construct($call, $able = '') { if ($call instanceof \Closure) { $this->_callback = $call; return; } if (!\is_string($able)) { throw new Exception('Bad callback form; the able part must be a string.', 0); } if ('' === $able) { if (\is_string($call)) { if (false === \strpos($call, '::')) { if (!\function_exists($call)) { throw new Exception('Bad callback form; function %s does not exist.', 1, $call); } $this->_callback = $call; return; } list($call, $able) = \explode('::', $call); } elseif (\is_object($call)) { if ($call instanceof StreamOut) { $able = null; } elseif (\method_exists($call, '__invoke')) { $able = '__invoke'; } else { throw new Exception('Bad callback form; an object but without a known '.'method.', 2); } } elseif (\is_array($call) && isset($call[0])) { if (!isset($call[1])) { $this->__construct($call[0]); return; } $this->__construct($call[0], $call[1]); return; } else { throw new Exception('Bad callback form.', 3); } } $this->_callback = [$call, $able]; return; } /** * Calls the callable. */ public function __invoke(...$arguments) { $callback = $this->getValidCallback($arguments); return $callback(...$arguments); } /** * Returns a valid PHP callback. */ public function getValidCallback(array &$arguments = []) { $callback = $this->_callback; $head = null; if (isset($arguments[0])) { $head = &$arguments[0]; } // If method is undetermined, we find it (we understand event bucket and // stream). if (null !== $head && \is_array($callback) && null === $callback[1]) { if ($head instanceof EventBucket) { $head = $head->getData(); } switch ($type = \gettype($head)) { case 'string': if (1 === \strlen($head)) { $method = 'writeCharacter'; } else { $method = 'writeString'; } break; case 'boolean': case 'integer': case 'array': $method = 'write'.\ucfirst($type); break; case 'double': $method = 'writeFloat'; break; default: $method = 'writeAll'; $head = $head."\n"; } $callback[1] = $method; } return $callback; } /** * Computes the hash of this callable. * * Will produce: * * `function#…`, * * `class#…::…`, * * `object(…)#…::…`, * * `closure(…)`. */ public function getHash(): string { if (null !== $this->_hash) { return $this->_hash; } $_ = &$this->_callback; if (\is_string($_)) { return $this->_hash = 'function#'.$_; } if (\is_array($_)) { return $this->_hash = (\is_object($_[0]) ? 'object('.\spl_object_hash($_[0]).')'. '#'.\get_class($_[0]) : 'class#'.$_[0]). '::'. (null !== $_[1] ? $_[1] : '???'); } return $this->_hash = 'closure('.\spl_object_hash($_).')'; } /** * The string representation of a callable is its hash. */ public function __toString(): string { return $this->getHash(); } /** * Hoa's xcallable() helper. */ public static function from($call, $able = '') { if ($call instanceof self) { return $call; } return new self($call, $able); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/StreamStatable.php���������������������������������������������������������������������0000644�����������������00000005767�15025056514�0012444 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Statable. * * Interface for statable input/output. */ interface StreamStatable extends IStream { /** * Size is undefined. */ const SIZE_UNDEFINED = -1; /** * Get size. */ public function getSize(): int; /** * Get informations about a file. */ public function getStatistic(): array; /** * Get last access time of file. */ public function getATime(): int; /** * Get inode change time of file. */ public function getCTime(): int; /** * Get file modification time. */ public function getMTime(): int; /** * Get file group. */ public function getGroup(): int; /** * Get file owner. */ public function getOwner(): int; /** * Get file permissions. */ public function getPermissions(): int; /** * Check if the file is readable. */ public function isReadable(): bool; /** * Check if the file is writable. */ public function isWritable(): bool; /** * Check if the file is executable. */ public function isExecutable(): bool; /** * Clear file status cache. */ public function clearStatisticCache(); /** * Clear all files status cache. */ public static function clearAllStatisticCaches(); } ���������Readline/Hoa/Ustring.php����������������������������������������������������������������������������0000644�����������������00000012012�15025056514�0011141 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * This class represents a UTF-8 string. * Please, see: * * http://www.ietf.org/rfc/rfc3454.txt, * * http://unicode.org/reports/tr9/, * * http://www.unicode.org/Public/6.0.0/ucd/UnicodeData.txt. */ class Ustring { /** * Check if ext/mbstring is available. */ public static function checkMbString(): bool { return \function_exists('mb_substr'); } /** * Get the number of column positions of a wide-character. * * This is a PHP implementation of wcwidth() and wcswidth() (defined in IEEE * Std 1002.1-2001) for Unicode, by Markus Kuhn. Please, see * http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. * * The wcwidth(wc) function shall either return 0 (if wc is a null * wide-character code), or return the number of column positions to be * occupied by the wide-character code wc, or return -1 (if wc does not * correspond to a printable wide-character code). */ public static function getCharWidth(string $char): int { $char = (string) $char; $c = static::toCode($char); // Test for 8-bit control characters. if (0x0 === $c) { return 0; } if (0x20 > $c || (0x7F <= $c && $c < 0xA0)) { return -1; } // Non-spacing characters. if (0xAD !== $c && 0 !== \preg_match('#^[\p{Mn}\p{Me}\p{Cf}\x{1160}-\x{11ff}\x{200b}]#u', $char)) { return 0; } // If we arrive here, $c is not a combining C0/C1 control character. return 1 + (0x1100 <= $c && (0x115F >= $c || // Hangul Jamo init. consonants 0x2329 === $c || 0x232A === $c || (0x2E80 <= $c && 0xA4CF >= $c && 0x303F !== $c) || // CJK…Yi (0xAC00 <= $c && 0xD7A3 >= $c) || // Hangul Syllables (0xF900 <= $c && 0xFAFF >= $c) || // CJK Compatibility Ideographs (0xFE10 <= $c && 0xFE19 >= $c) || // Vertical forms (0xFE30 <= $c && 0xFE6F >= $c) || // CJK Compatibility Forms (0xFF00 <= $c && 0xFF60 >= $c) || // Fullwidth Forms (0xFFE0 <= $c && 0xFFE6 >= $c) || (0x20000 <= $c && 0x2FFFD >= $c) || (0x30000 <= $c && 0x3FFFD >= $c))); } /** * Check whether the character is printable or not. */ public static function isCharPrintable(string $char): bool { return 1 <= static::getCharWidth($char); } /** * Get a decimal code representation of a specific character. */ public static function toCode(string $char): int { $char = (string) $char; $code = \ord($char[0]); $bytes = 1; if (!($code & 0x80)) { // 0xxxxxxx return $code; } if (($code & 0xE0) === 0xC0) { // 110xxxxx $bytes = 2; $code = $code & ~0xC0; } elseif (($code & 0xF0) === 0xE0) { // 1110xxxx $bytes = 3; $code = $code & ~0xE0; } elseif (($code & 0xF8) === 0xF0) { // 11110xxx $bytes = 4; $code = $code & ~0xF0; } for ($i = 2; $i <= $bytes; $i++) { // 10xxxxxx $code = ($code << 6) + (\ord($char[$i - 1]) & ~0x80); } return $code; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Hoa/FileReadWrite.php����������������������������������������������������������������������0000644�����������������00000015461�15025056514�0012207 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\ReadWrite. * * File handler. */ class FileReadWrite extends File implements StreamIn, StreamOut { /** * Open a file. */ public function __construct( string $streamName, string $mode = parent::MODE_APPEND_READ_WRITE, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ_WRITE, parent::MODE_TRUNCATE_READ_WRITE, parent::MODE_APPEND_READ_WRITE, parent::MODE_CREATE_READ_WRITE, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName) && parent::MODE_READ_WRITE === $this->getMode()) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->read($length); } /** * Read a character. */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } /** * Write n characters. */ public function write(string $string, int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 3, $length); } return \fwrite($this->getStream(), $string, $length); } /** * Write a string. */ public function writeString(string $string) { $string = (string) $string; return $this->write($string, \strlen($string)); } /** * Write a character. */ public function writeCharacter(string $char) { return $this->write((string) $char[0], 1); } /** * Write a boolean. */ public function writeBoolean(bool $boolean) { return $this->write((string) (bool) $boolean, 1); } /** * Write an integer. */ public function writeInteger(int $integer) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer)); } /** * Write a float. */ public function writeFloat(float $float) { $float = (string) (float) $float; return $this->write($float, \strlen($float)); } /** * Write an array. */ public function writeArray(array $array) { $array = \var_export($array, true); return $this->write($array, \strlen($array)); } /** * Write a line. */ public function writeLine(string $line) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1); } ++$n; return $this->write(\substr($line, 0, $n), $n); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string) { return $this->write($string, \strlen($string)); } /** * Truncate a file to a given length. */ public function truncate(int $size): bool { return \ftruncate($this->getStream(), $size); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Readline/Libedit.php��������������������������������������������������������������������������������0000644�����������������00000006470�15025056514�0010366 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Readline; use Psy\Util\Str; /** * A Libedit-based Readline implementation. * * This is largely the same as the Readline implementation, but it emulates * support for `readline_list_history` since PHP decided it was a good idea to * ship a fake Readline implementation that is missing history support. * * NOTE: As of PHP 7.4, PHP sometimes has history support in the Libedit * wrapper, so it will use the GNUReadline implementation rather than this one. */ class Libedit extends GNUReadline { private $hasWarnedOwnership = false; /** * Let's emulate GNU Readline by manually reading and parsing the history file! */ public static function isSupported(): bool { return \function_exists('readline') && !\function_exists('readline_list_history'); } /** * {@inheritdoc} */ public static function supportsBracketedPaste(): bool { return false; } /** * {@inheritdoc} */ public function listHistory(): array { $history = \file_get_contents($this->historyFile); if (!$history) { return []; } // libedit doesn't seem to support non-unix line separators. $history = \explode("\n", $history); // remove history signature if it exists if ($history[0] === '_HiStOrY_V2_') { \array_shift($history); } // decode the line $history = \array_map([$this, 'parseHistoryLine'], $history); // filter empty lines & comments return \array_values(\array_filter($history)); } /** * {@inheritdoc} */ public function writeHistory(): bool { $res = parent::writeHistory(); // Libedit apparently refuses to save history if the history file is not // owned by the user, even if it is writable. Warn when this happens. // // See https://github.com/bobthecow/psysh/issues/552 if ($res === false && !$this->hasWarnedOwnership) { if (\is_file($this->historyFile) && \is_writable($this->historyFile)) { $this->hasWarnedOwnership = true; $msg = \sprintf('Error writing history file, check file ownership: %s', $this->historyFile); \trigger_error($msg, \E_USER_NOTICE); } } return $res; } /** * From GNUReadline (readline/histfile.c & readline/histexpand.c): * lines starting with "\0" are comments or timestamps; * if "\0" is found in an entry, * everything from it until the next line is a comment. * * @param string $line The history line to parse * * @return string|null */ protected function parseHistoryLine(string $line) { // empty line, comment or timestamp if (!$line || $line[0] === "\0") { return; } // if "\0" is found in an entry, then // everything from it until the end of line is a comment. if (($pos = \strpos($line, "\0")) !== false) { $line = \substr($line, 0, $pos); } return ($line !== '') ? Str::unvis($line) : null; } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/CalledClassPass.php���������������������������������������������������������������������0000644�����������������00000005260�15025056514�0012430 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\VariadicPlaceholder; use Psy\Exception\ErrorException; /** * The called class pass throws warnings for get_class() and get_called_class() * outside a class context. */ class CalledClassPass extends CodeCleanerPass { private $inClass; /** * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->inClass = false; } /** * @throws ErrorException if get_class or get_called_class is called without an object from outside a class * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Class_ || $node instanceof Trait_) { $this->inClass = true; } elseif ($node instanceof FuncCall && !$this->inClass) { // We'll give any args at all (besides null) a pass. // Technically we should be checking whether the args are objects, but this will do for now. // // @todo switch this to actually validate args when we get context-aware code cleaner passes. if (!empty($node->args) && !$this->isNull($node->args[0])) { return; } // We'll ignore name expressions as well (things like `$foo()`) if (!($node->name instanceof Name)) { return; } $name = \strtolower($node->name); if (\in_array($name, ['get_class', 'get_called_class'])) { $msg = \sprintf('%s() called without object from outside a class', $name); throw new ErrorException($msg, 0, \E_USER_WARNING, null, $node->getLine()); } } } /** * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof Class_) { $this->inClass = false; } } private function isNull(Node $node): bool { if ($node instanceof VariadicPlaceholder) { return false; } return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null'; } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/NamespaceAwarePass.php������������������������������������������������������������������0000644�����������������00000003650�15025056514�0013133 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Stmt\Namespace_; /** * Abstract namespace-aware code cleaner pass. */ abstract class NamespaceAwarePass extends CodeCleanerPass { protected $namespace; protected $currentScope; /** * @todo should this be final? Extending classes should be sure to either * use afterTraverse or call parent::beforeTraverse() when overloading. * * Reset the namespace and the current scope before beginning analysis * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->namespace = []; $this->currentScope = []; } /** * @todo should this be final? Extending classes should be sure to either use * leaveNode or call parent::enterNode() when overloading * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Namespace_) { $this->namespace = isset($node->name) ? $node->name->parts : []; } } /** * Get a fully-qualified name (class, function, interface, etc). * * @param mixed $name */ protected function getFullyQualifiedName($name): string { if ($name instanceof FullyQualifiedName) { return \implode('\\', $name->parts); } elseif ($name instanceof Name) { $name = $name->parts; } elseif (!\is_array($name)) { $name = [$name]; } return \implode('\\', \array_merge($this->namespace, $name)); } } ����������������������������������������������������������������������������������������CodeCleaner/IssetPass.php���������������������������������������������������������������������������0000644�����������������00000002571�15025056514�0011347 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\NullsafePropertyFetch; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; use Psy\Exception\FatalErrorException; /** * Code cleaner pass to ensure we only allow variables, array fetch and property * fetch expressions in isset() calls. */ class IssetPass extends CodeCleanerPass { const EXCEPTION_MSG = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)'; /** * @throws FatalErrorException * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if (!$node instanceof Isset_) { return; } foreach ($node->vars as $var) { if (!$var instanceof Variable && !$var instanceof ArrayDimFetch && !$var instanceof PropertyFetch && !$var instanceof NullsafePropertyFetch) { throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getLine()); } } } } ���������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/StrictTypesPass.php���������������������������������������������������������������������0000644�����������������00000004753�15025056514�0012561 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\DeclareDeclare; use Psy\Exception\FatalErrorException; /** * Provide implicit strict types declarations for for subsequent execution. * * The strict types pass remembers the last strict types declaration: * * declare(strict_types=1); * * ... which it then applies implicitly to all future evaluated code, until it * is replaced by a new declaration. */ class StrictTypesPass extends CodeCleanerPass { const EXCEPTION_MESSAGE = 'strict_types declaration must have 0 or 1 as its value'; private $strictTypes = false; /** * If this is a standalone strict types declaration, remember it for later. * * Otherwise, apply remembered strict types declaration to to the code until * a new declaration is encountered. * * @throws FatalErrorException if an invalid `strict_types` declaration is found * * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $prependStrictTypes = $this->strictTypes; foreach ($nodes as $node) { if ($node instanceof Declare_) { foreach ($node->declares as $declare) { // For PHP Parser 4.x $declareKey = $declare->key instanceof Identifier ? $declare->key->toString() : $declare->key; if ($declareKey === 'strict_types') { $value = $declare->value; if (!$value instanceof LNumber || ($value->value !== 0 && $value->value !== 1)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } $this->strictTypes = $value->value === 1; } } } } if ($prependStrictTypes) { $first = \reset($nodes); if (!$first instanceof Declare_) { $declare = new Declare_([new DeclareDeclare('strict_types', new LNumber(1))]); \array_unshift($nodes, $declare); } } return $nodes; } } ���������������������CodeCleaner/ImplicitReturnPass.php������������������������������������������������������������������0000644�����������������00000010211�15025056514�0013220 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Exit_; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Switch_; /** * Add an implicit "return" to the last statement, provided it can be returned. */ class ImplicitReturnPass extends CodeCleanerPass { /** * @param array $nodes * * @return array */ public function beforeTraverse(array $nodes): array { return $this->addImplicitReturn($nodes); } /** * @param array $nodes * * @return array */ private function addImplicitReturn(array $nodes): array { // If nodes is empty, it can't have a return value. if (empty($nodes)) { return [new Return_(NoReturnValue::create())]; } $last = \end($nodes); // Special case a few types of statements to add an implicit return // value (even though they technically don't have any return value) // because showing a return value in these instances is useful and not // very surprising. if ($last instanceof If_) { $last->stmts = $this->addImplicitReturn($last->stmts); foreach ($last->elseifs as $elseif) { $elseif->stmts = $this->addImplicitReturn($elseif->stmts); } if ($last->else) { $last->else->stmts = $this->addImplicitReturn($last->else->stmts); } } elseif ($last instanceof Switch_) { foreach ($last->cases as $case) { // only add an implicit return to cases which end in break $caseLast = \end($case->stmts); if ($caseLast instanceof Break_) { $case->stmts = $this->addImplicitReturn(\array_slice($case->stmts, 0, -1)); $case->stmts[] = $caseLast; } } } elseif ($last instanceof Expr && !($last instanceof Exit_)) { // @codeCoverageIgnoreStart $nodes[\count($nodes) - 1] = new Return_($last, [ 'startLine' => $last->getLine(), 'endLine' => $last->getLine(), ]); // @codeCoverageIgnoreEnd } elseif ($last instanceof Expression && !($last->expr instanceof Exit_)) { // For PHP Parser 4.x $nodes[\count($nodes) - 1] = new Return_($last->expr, [ 'startLine' => $last->getLine(), 'endLine' => $last->getLine(), ]); } elseif ($last instanceof Namespace_) { $last->stmts = $this->addImplicitReturn($last->stmts); } // Return a "no return value" for all non-expression statements, so that // PsySH can suppress the `null` that `eval()` returns otherwise. // // Note that statements special cased above (if/elseif/else, switch) // _might_ implicitly return a value before this catch-all return is // reached. // // We're not adding a fallback return after namespace statements, // because code outside namespace statements doesn't really work, and // there's already an implicit return in the namespace statement anyway. if (self::isNonExpressionStmt($last)) { $nodes[] = new Return_(NoReturnValue::create()); } return $nodes; } /** * Check whether a given node is a non-expression statement. * * As of PHP Parser 4.x, Expressions are now instances of Stmt as well, so * we'll exclude them here. * * @param Node $node */ private static function isNonExpressionStmt(Node $node): bool { return $node instanceof Stmt && !$node instanceof Expression && !$node instanceof Return_ && !$node instanceof Namespace_; } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/CodeCleanerPass.php���������������������������������������������������������������������0000644�����������������00000000637�15025056514�0012425 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\NodeVisitorAbstract; /** * A CodeCleaner pass is a PhpParser Node Visitor. */ abstract class CodeCleanerPass extends NodeVisitorAbstract { // Wheee! } �������������������������������������������������������������������������������������������������CodeCleaner/CallTimePassByReferencePass.php���������������������������������������������������������0000644�����������������00000003107�15025056514�0014707 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\VariadicPlaceholder; use Psy\Exception\FatalErrorException; /** * Validate that the user did not use the call-time pass-by-reference that causes a fatal error. * * As of PHP 5.4.0, call-time pass-by-reference was removed, so using it will raise a fatal error. * * @author Martin Hasoň <martin.hason@gmail.com> */ class CallTimePassByReferencePass extends CodeCleanerPass { const EXCEPTION_MESSAGE = 'Call-time pass-by-reference has been removed'; /** * Validate of use call-time pass-by-reference. * * @throws FatalErrorException if the user used call-time pass-by-reference * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) { return; } foreach ($node->args as $arg) { if ($arg instanceof VariadicPlaceholder) { continue; } if ($arg->byRef) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/LeavePsyshAlonePass.php�����������������������������������������������������������������0000644�����������������00000001740�15025056514�0013317 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Variable; use Psy\Exception\RuntimeException; /** * Validate that the user input does not reference the `$__psysh__` variable. */ class LeavePsyshAlonePass extends CodeCleanerPass { /** * Validate that the user input does not reference the `$__psysh__` variable. * * @throws RuntimeException if the user is messing with $__psysh__ * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Variable && $node->name === '__psysh__') { throw new RuntimeException('Don\'t mess with $__psysh__; bad things will happen'); } } } ��������������������������������CodeCleaner/InstanceOfPass.php����������������������������������������������������������������������0000644�����������������00000003645�15025056514�0012314 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\BinaryOp; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Scalar; use PhpParser\Node\Scalar\Encapsed; use Psy\Exception\FatalErrorException; /** * Validate that the instanceof statement does not receive a scalar value or a non-class constant. * * @author Martin Hasoň <martin.hason@gmail.com> */ class InstanceOfPass extends CodeCleanerPass { const EXCEPTION_MSG = 'instanceof expects an object instance, constant given'; private $atLeastPhp73; public function __construct() { $this->atLeastPhp73 = \version_compare(\PHP_VERSION, '7.3', '>='); } /** * Validate that the instanceof statement does not receive a scalar value or a non-class constant. * * @throws FatalErrorException if a scalar or a non-class constant is given * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { // Basically everything is allowed in PHP 7.3 :) if ($this->atLeastPhp73) { return; } if (!$node instanceof Instanceof_) { return; } if (($node->expr instanceof Scalar && !$node->expr instanceof Encapsed) || $node->expr instanceof BinaryOp || $node->expr instanceof Array_ || $node->expr instanceof ConstFetch || $node->expr instanceof ClassConstFetch ) { throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getLine()); } } } �������������������������������������������������������������������������������������������CodeCleaner/ValidClassNamePass.php������������������������������������������������������������������0000644�����������������00000023552�15025056514�0013110 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Ternary; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Do_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\Stmt\While_; use Psy\Exception\FatalErrorException; /** * Validate that classes exist. * * This pass throws a FatalErrorException rather than letting PHP run * headfirst into a real fatal error and die. */ class ValidClassNamePass extends NamespaceAwarePass { const CLASS_TYPE = 'class'; const INTERFACE_TYPE = 'interface'; const TRAIT_TYPE = 'trait'; private $conditionalScopes = 0; /** * Validate class, interface and trait definitions. * * Validate them upon entering the node, so that we know about their * presence and can validate constant fetches and static calls in class or * trait methods. * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { parent::enterNode($node); if (self::isConditional($node)) { $this->conditionalScopes++; return; } if ($this->conditionalScopes === 0) { if ($node instanceof Class_) { $this->validateClassStatement($node); } elseif ($node instanceof Interface_) { $this->validateInterfaceStatement($node); } elseif ($node instanceof Trait_) { $this->validateTraitStatement($node); } } } /** * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if (self::isConditional($node)) { $this->conditionalScopes--; return; } } private static function isConditional(Node $node): bool { return $node instanceof If_ || $node instanceof While_ || $node instanceof Do_ || $node instanceof Switch_ || $node instanceof Ternary; } /** * Validate a class definition statement. * * @param Class_ $stmt */ protected function validateClassStatement(Class_ $stmt) { $this->ensureCanDefine($stmt, self::CLASS_TYPE); if (isset($stmt->extends)) { $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt); } $this->ensureInterfacesExist($stmt->implements, $stmt); } /** * Validate an interface definition statement. * * @param Interface_ $stmt */ protected function validateInterfaceStatement(Interface_ $stmt) { $this->ensureCanDefine($stmt, self::INTERFACE_TYPE); $this->ensureInterfacesExist($stmt->extends, $stmt); } /** * Validate a trait definition statement. * * @param Trait_ $stmt */ protected function validateTraitStatement(Trait_ $stmt) { $this->ensureCanDefine($stmt, self::TRAIT_TYPE); } /** * Ensure that no class, interface or trait name collides with a new definition. * * @throws FatalErrorException * * @param Stmt $stmt * @param string $scopeType */ protected function ensureCanDefine(Stmt $stmt, string $scopeType = self::CLASS_TYPE) { // Anonymous classes don't have a name, and uniqueness shouldn't be enforced. if ($stmt->name === null) { return; } $name = $this->getFullyQualifiedName($stmt->name); // check for name collisions $errorType = null; if ($this->classExists($name)) { $errorType = self::CLASS_TYPE; } elseif ($this->interfaceExists($name)) { $errorType = self::INTERFACE_TYPE; } elseif ($this->traitExists($name)) { $errorType = self::TRAIT_TYPE; } if ($errorType !== null) { throw $this->createError(\sprintf('%s named %s already exists', \ucfirst($errorType), $name), $stmt); } // Store creation for the rest of this code snippet so we can find local // issue too $this->currentScope[\strtolower($name)] = $scopeType; } /** * Ensure that a referenced class exists. * * @throws FatalErrorException * * @param string $name * @param Stmt $stmt */ protected function ensureClassExists(string $name, Stmt $stmt) { if (!$this->classExists($name)) { throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); } } /** * Ensure that a referenced class _or interface_ exists. * * @throws FatalErrorException * * @param string $name * @param Stmt $stmt */ protected function ensureClassOrInterfaceExists(string $name, Stmt $stmt) { if (!$this->classExists($name) && !$this->interfaceExists($name)) { throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); } } /** * Ensure that a referenced class _or trait_ exists. * * @throws FatalErrorException * * @param string $name * @param Stmt $stmt */ protected function ensureClassOrTraitExists(string $name, Stmt $stmt) { if (!$this->classExists($name) && !$this->traitExists($name)) { throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); } } /** * Ensure that a statically called method exists. * * @throws FatalErrorException * * @param string $class * @param string $name * @param Stmt $stmt */ protected function ensureMethodExists(string $class, string $name, Stmt $stmt) { $this->ensureClassOrTraitExists($class, $stmt); // let's pretend all calls to self, parent and static are valid if (\in_array(\strtolower($class), ['self', 'parent', 'static'])) { return; } // ... and all calls to classes defined right now if ($this->findInScope($class) === self::CLASS_TYPE) { return; } // if method name is an expression, give it a pass for now if ($name instanceof Expr) { return; } if (!\method_exists($class, $name) && !\method_exists($class, '__callStatic')) { throw $this->createError(\sprintf('Call to undefined method %s::%s()', $class, $name), $stmt); } } /** * Ensure that a referenced interface exists. * * @throws FatalErrorException * * @param Interface_[] $interfaces * @param Stmt $stmt */ protected function ensureInterfacesExist(array $interfaces, Stmt $stmt) { foreach ($interfaces as $interface) { /** @var string $name */ $name = $this->getFullyQualifiedName($interface); if (!$this->interfaceExists($name)) { throw $this->createError(\sprintf('Interface \'%s\' not found', $name), $stmt); } } } /** * Get a symbol type key for storing in the scope name cache. * * @deprecated No longer used. Scope type should be passed into ensureCanDefine directly. * * @codeCoverageIgnore * * @throws FatalErrorException * * @param Stmt $stmt */ protected function getScopeType(Stmt $stmt): string { if ($stmt instanceof Class_) { return self::CLASS_TYPE; } elseif ($stmt instanceof Interface_) { return self::INTERFACE_TYPE; } elseif ($stmt instanceof Trait_) { return self::TRAIT_TYPE; } throw $this->createError('Unsupported statement type', $stmt); } /** * Check whether a class exists, or has been defined in the current code snippet. * * Gives `self`, `static` and `parent` a free pass. * * @param string $name */ protected function classExists(string $name): bool { // Give `self`, `static` and `parent` a pass. This will actually let // some errors through, since we're not checking whether the keyword is // being used in a class scope. if (\in_array(\strtolower($name), ['self', 'static', 'parent'])) { return true; } return \class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE; } /** * Check whether an interface exists, or has been defined in the current code snippet. * * @param string $name */ protected function interfaceExists(string $name): bool { return \interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE; } /** * Check whether a trait exists, or has been defined in the current code snippet. * * @param string $name */ protected function traitExists(string $name): bool { return \trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE; } /** * Find a symbol in the current code snippet scope. * * @param string $name * * @return string|null */ protected function findInScope(string $name) { $name = \strtolower($name); if (isset($this->currentScope[$name])) { return $this->currentScope[$name]; } } /** * Error creation factory. * * @param string $msg * @param Stmt $stmt */ protected function createError(string $msg, Stmt $stmt): FatalErrorException { return new FatalErrorException($msg, 0, \E_ERROR, null, $stmt->getLine()); } } ������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/AbstractClassPass.php�������������������������������������������������������������������0000644�����������������00000004477�15025056514�0013020 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use Psy\Exception\FatalErrorException; /** * The abstract class pass handles abstract classes and methods, complaining if there are too few or too many of either. */ class AbstractClassPass extends CodeCleanerPass { private $class; private $abstractMethods; /** * @throws FatalErrorException if the node is an abstract function with a body * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Class_) { $this->class = $node; $this->abstractMethods = []; } elseif ($node instanceof ClassMethod) { if ($node->isAbstract()) { $name = \sprintf('%s::%s', $this->class->name, $node->name); $this->abstractMethods[] = $name; if ($node->stmts !== null) { $msg = \sprintf('Abstract function %s cannot contain body', $name); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } } } /** * @throws FatalErrorException if the node is a non-abstract class with abstract methods * * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof Class_) { $count = \count($this->abstractMethods); if ($count > 0 && !$node->isAbstract()) { $msg = \sprintf( 'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)', $node->name, $count, ($count === 1) ? '' : 's', \implode(', ', $this->abstractMethods) ); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/AssignThisVariablePass.php��������������������������������������������������������������0000644�����������������00000002132�15025056514�0013773 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Variable; use Psy\Exception\FatalErrorException; /** * Validate that the user input does not assign the `$this` variable. * * @author Martin Hasoň <martin.hason@gmail.com> */ class AssignThisVariablePass extends CodeCleanerPass { /** * Validate that the user input does not assign the `$this` variable. * * @throws FatalErrorException if the user assign the `$this` variable * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Assign && $node->var instanceof Variable && $node->var->name === 'this') { throw new FatalErrorException('Cannot re-assign $this', 0, \E_ERROR, null, $node->getLine()); } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/ListPass.php����������������������������������������������������������������������������0000644�����������������00000006415�15025056514�0011174 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\List_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; use Psy\Exception\ParseErrorException; /** * Validate that the list assignment. */ class ListPass extends CodeCleanerPass { private $atLeastPhp71; public function __construct() { $this->atLeastPhp71 = \version_compare(\PHP_VERSION, '7.1', '>='); } /** * Validate use of list assignment. * * @throws ParseErrorException if the user used empty with anything but a variable * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if (!$node instanceof Assign) { return; } if (!$node->var instanceof Array_ && !$node->var instanceof List_) { return; } if (!$this->atLeastPhp71 && $node->var instanceof Array_) { $msg = "syntax error, unexpected '='"; throw new ParseErrorException($msg, $node->expr->getLine()); } // Polyfill for PHP-Parser 2.x $items = isset($node->var->items) ? $node->var->items : $node->var->vars; if ($items === [] || $items === [null]) { throw new ParseErrorException('Cannot use empty list', $node->var->getLine()); } $itemFound = false; foreach ($items as $item) { if ($item === null) { continue; } $itemFound = true; // List_->$vars in PHP-Parser 2.x is Variable instead of ArrayItem. if (!$this->atLeastPhp71 && $item instanceof ArrayItem && $item->key !== null) { $msg = 'Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting \',\' or \')\''; throw new ParseErrorException($msg, $item->key->getLine()); } if (!self::isValidArrayItem($item)) { $msg = 'Assignments can only happen to writable values'; throw new ParseErrorException($msg, $item->getLine()); } } if (!$itemFound) { throw new ParseErrorException('Cannot use empty list'); } } /** * Validate whether a given item in an array is valid for short assignment. * * @param Expr $item */ private static function isValidArrayItem(Expr $item): bool { $value = ($item instanceof ArrayItem) ? $item->value : $item; while ($value instanceof ArrayDimFetch || $value instanceof PropertyFetch) { $value = $value->var; } // We just kind of give up if it's a method call. We can't tell if it's // valid via static analysis. return $value instanceof Variable || $value instanceof MethodCall || $value instanceof FuncCall; } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/PassableByReferencePass.php�������������������������������������������������������������0000644�����������������00000010152�15025056514�0014116 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; use Psy\Exception\FatalErrorException; /** * Validate that only variables (and variable-like things) are passed by reference. */ class PassableByReferencePass extends CodeCleanerPass { const EXCEPTION_MESSAGE = 'Only variables can be passed by reference'; /** * @throws FatalErrorException if non-variables are passed by reference * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { // @todo support MethodCall and StaticCall as well. if ($node instanceof FuncCall) { // if function name is an expression or a variable, give it a pass for now. if ($node->name instanceof Expr || $node->name instanceof Variable) { return; } $name = (string) $node->name; if ($name === 'array_multisort') { return $this->validateArrayMultisort($node); } try { $refl = new \ReflectionFunction($name); } catch (\ReflectionException $e) { // Well, we gave it a shot! return; } foreach ($refl->getParameters() as $key => $param) { if (\array_key_exists($key, $node->args)) { $arg = $node->args[$key]; if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } } } } } private function isPassableByReference(Node $arg): bool { // Unpacked arrays can be passed by reference if ($arg->value instanceof Array_) { return $arg->unpack; } // FuncCall, MethodCall and StaticCall are all PHP _warnings_ not fatal errors, so we'll let // PHP handle those ones :) return $arg->value instanceof ClassConstFetch || $arg->value instanceof PropertyFetch || $arg->value instanceof Variable || $arg->value instanceof FuncCall || $arg->value instanceof MethodCall || $arg->value instanceof StaticCall || $arg->value instanceof ArrayDimFetch; } /** * Because array_multisort has a problematic signature... * * The argument order is all sorts of wonky, and whether something is passed * by reference or not depends on the values of the two arguments before it. * We'll do a good faith attempt at validating this, but err on the side of * permissive. * * This is why you don't design languages where core code and extensions can * implement APIs that wouldn't be possible in userland code. * * @throws FatalErrorException for clearly invalid arguments * * @param Node $node */ private function validateArrayMultisort(Node $node) { $nonPassable = 2; // start with 2 because the first one has to be passable by reference foreach ($node->args as $arg) { if ($this->isPassableByReference($arg)) { $nonPassable = 0; } elseif (++$nonPassable > 2) { // There can be *at most* two non-passable-by-reference args in a row. This is about // as close as we can get to validating the arguments for this function :-/ throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/MagicConstantsPass.php������������������������������������������������������������������0000644�����������������00000002054�15025056514�0013171 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PhpParser\Node\Scalar\MagicConst\Dir; use PhpParser\Node\Scalar\MagicConst\File; use PhpParser\Node\Scalar\String_; /** * Swap out __DIR__ and __FILE__ magic constants with our best guess? */ class MagicConstantsPass extends CodeCleanerPass { /** * Swap out __DIR__ and __FILE__ constants, because the default ones when * calling eval() don't make sense. * * @param Node $node * * @return FuncCall|String_|null */ public function enterNode(Node $node) { if ($node instanceof Dir) { return new FuncCall(new Name('getcwd'), [], $node->getAttributes()); } elseif ($node instanceof File) { return new String_('', $node->getAttributes()); } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/LabelContextPass.php��������������������������������������������������������������������0000644�����������������00000005116�15025056514�0012642 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\Goto_; use PhpParser\Node\Stmt\Label; use Psy\Exception\FatalErrorException; /** * CodeCleanerPass for label context. * * This class partially emulates the PHP label specification. * PsySH can not declare labels by sequentially executing lines with eval, * but since it is not a syntax error, no error is raised. * This class warns before invalid goto causes a fatal error. * Since this is a simple checker, it does not block real fatal error * with complex syntax. (ex. it does not parse inside function.) * * @see http://php.net/goto */ class LabelContextPass extends CodeCleanerPass { /** @var int */ private $functionDepth; /** @var array */ private $labelDeclarations; /** @var array */ private $labelGotos; /** * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->functionDepth = 0; $this->labelDeclarations = []; $this->labelGotos = []; } /** * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth++; return; } // node is inside function context if ($this->functionDepth !== 0) { return; } if ($node instanceof Goto_) { $this->labelGotos[\strtolower($node->name)] = $node->getLine(); } elseif ($node instanceof Label) { $this->labelDeclarations[\strtolower($node->name)] = $node->getLine(); } } /** * @param \PhpParser\Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth--; } } /** * @return Node[]|null Array of nodes */ public function afterTraverse(array $nodes) { foreach ($this->labelGotos as $name => $line) { if (!isset($this->labelDeclarations[$name])) { $msg = "'goto' to undefined label '{$name}'"; throw new FatalErrorException($msg, 0, \E_ERROR, null, $line); } } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/LoopContextPass.php���������������������������������������������������������������������0000644�����������������00000007156�15025056514�0012542 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Scalar\DNumber; use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; use PhpParser\Node\Stmt\Do_; use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\While_; use Psy\Exception\FatalErrorException; /** * The loop context pass handles invalid `break` and `continue` statements. */ class LoopContextPass extends CodeCleanerPass { private $loopDepth; /** * {@inheritdoc} * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->loopDepth = 0; } /** * @throws FatalErrorException if the node is a break or continue in a non-loop or switch context * @throws FatalErrorException if the node is trying to break out of more nested structures than exist * @throws FatalErrorException if the node is a break or continue and has a non-numeric argument * @throws FatalErrorException if the node is a break or continue and has an argument less than 1 * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { switch (true) { case $node instanceof Do_: case $node instanceof For_: case $node instanceof Foreach_: case $node instanceof Switch_: case $node instanceof While_: $this->loopDepth++; break; case $node instanceof Break_: case $node instanceof Continue_: $operator = $node instanceof Break_ ? 'break' : 'continue'; if ($this->loopDepth === 0) { $msg = \sprintf("'%s' not in the 'loop' or 'switch' context", $operator); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } if ($node->num instanceof LNumber || $node->num instanceof DNumber) { $num = $node->num->value; if ($node->num instanceof DNumber || $num < 1) { $msg = \sprintf("'%s' operator accepts only positive numbers", $operator); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } if ($num > $this->loopDepth) { $msg = \sprintf("Cannot '%s' %d levels", $operator, $num); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } elseif ($node->num) { $msg = \sprintf("'%s' operator with non-constant operand is no longer supported", $operator); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } break; } } /** * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { switch (true) { case $node instanceof Do_: case $node instanceof For_: case $node instanceof Foreach_: case $node instanceof Switch_: case $node instanceof While_: $this->loopDepth--; break; } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/ValidFunctionNamePass.php���������������������������������������������������������������0000644�����������������00000004563�15025056514�0013631 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Stmt\Do_; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\While_; use Psy\Exception\FatalErrorException; /** * Validate that function calls will succeed. * * This pass throws a FatalErrorException rather than letting PHP run * headfirst into a real fatal error and die. */ class ValidFunctionNamePass extends NamespaceAwarePass { private $conditionalScopes = 0; /** * Store newly defined function names on the way in, to allow recursion. * * @throws FatalErrorException if a function is redefined in a non-conditional scope * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { parent::enterNode($node); if (self::isConditional($node)) { $this->conditionalScopes++; } elseif ($node instanceof Function_) { $name = $this->getFullyQualifiedName($node->name); // @todo add an "else" here which adds a runtime check for instances where we can't tell // whether a function is being redefined by static analysis alone. if ($this->conditionalScopes === 0) { if (\function_exists($name) || isset($this->currentScope[\strtolower($name)])) { $msg = \sprintf('Cannot redeclare %s()', $name); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } $this->currentScope[\strtolower($name)] = true; } } /** * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if (self::isConditional($node)) { $this->conditionalScopes--; } } private static function isConditional(Node $node) { return $node instanceof If_ || $node instanceof While_ || $node instanceof Do_ || $node instanceof Switch_; } } ���������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/FinalClassPass.php����������������������������������������������������������������������0000644�����������������00000003365�15025056514�0012301 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use Psy\Exception\FatalErrorException; /** * The final class pass handles final classes. */ class FinalClassPass extends CodeCleanerPass { private $finalClasses; /** * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->finalClasses = []; } /** * @throws FatalErrorException if the node is a class that extends a final class * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Class_) { if ($node->extends) { $extends = (string) $node->extends; if ($this->isFinalClass($extends)) { $msg = \sprintf('Class %s may not inherit from final class (%s)', $node->name, $extends); throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } if ($node->isFinal()) { $this->finalClasses[\strtolower($node->name)] = true; } } } /** * @param string $name Class name */ private function isFinalClass(string $name): bool { if (!\class_exists($name)) { return isset($this->finalClasses[\strtolower($name)]); } $refl = new \ReflectionClass($name); return $refl->isFinal(); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/EmptyArrayDimFetchPass.php��������������������������������������������������������������0000644�����������������00000002614�15025056514�0013757 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use Psy\Exception\FatalErrorException; /** * Validate empty brackets are only used for assignment. */ class EmptyArrayDimFetchPass extends CodeCleanerPass { const EXCEPTION_MESSAGE = 'Cannot use [] for reading'; private $theseOnesAreFine = []; /** * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->theseOnesAreFine = []; } /** * @throws FatalErrorException if the user used empty empty array dim fetch outside of assignment * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Assign && $node->var instanceof ArrayDimFetch) { $this->theseOnesAreFine[] = $node->var; } if ($node instanceof ArrayDimFetch && $node->dim === null) { if (!\in_array($node, $this->theseOnesAreFine)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, $node->getLine()); } } } } ��������������������������������������������������������������������������������������������������������������������CodeCleaner/ExitPass.php����������������������������������������������������������������������������0000644�����������������00000001533�15025056514�0011166 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Exit_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use Psy\Exception\BreakException; class ExitPass extends CodeCleanerPass { /** * Converts exit calls to BreakExceptions. * * @param \PhpParser\Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof Exit_) { return new StaticCall(new FullyQualifiedName(BreakException::class), 'exitShell'); } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/NoReturnValue.php�����������������������������������������������������������������������0000644�����������������00000001474�15025056514�0012203 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node\Expr\New_; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; /** * A class used internally by CodeCleaner to represent input, such as * non-expression statements, with no return value. * * Note that user code returning an instance of this class will act like it * has no return value, so you prolly shouldn't do that. */ class NoReturnValue { /** * Get PhpParser AST expression for creating a new NoReturnValue. */ public static function create(): New_ { return new New_(new FullyQualifiedName(self::class)); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/ValidConstructorPass.php����������������������������������������������������������������0000644�����������������00000007667�15025056514�0013600 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Namespace_; use Psy\Exception\FatalErrorException; /** * Validate that the constructor method is not static, and does not have a * return type. * * Checks both explicit __construct methods as well as old-style constructor * methods with the same name as the class (for non-namespaced classes). * * As of PHP 5.3.3, methods with the same name as the last element of a * namespaced class name will no longer be treated as constructor. This change * doesn't affect non-namespaced classes. * * @author Martin Hasoň <martin.hason@gmail.com> */ class ValidConstructorPass extends CodeCleanerPass { private $namespace; /** * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->namespace = []; } /** * Validate that the constructor is not static and does not have a return type. * * @throws FatalErrorException the constructor function is static * @throws FatalErrorException the constructor function has a return type * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Namespace_) { $this->namespace = isset($node->name) ? $node->name->parts : []; } elseif ($node instanceof Class_) { $constructor = null; foreach ($node->stmts as $stmt) { if ($stmt instanceof ClassMethod) { // If we find a new-style constructor, no need to look for the old-style if ('__construct' === \strtolower($stmt->name)) { $this->validateConstructor($stmt, $node); return; } // We found a possible old-style constructor (unless there is also a __construct method) if (empty($this->namespace) && \strtolower($node->name) === \strtolower($stmt->name)) { $constructor = $stmt; } } } if ($constructor) { $this->validateConstructor($constructor, $node); } } } /** * @throws FatalErrorException the constructor function is static * @throws FatalErrorException the constructor function has a return type * * @param Node $constructor * @param Node $classNode */ private function validateConstructor(Node $constructor, Node $classNode) { if ($constructor->isStatic()) { // For PHP Parser 4.x $className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name; $msg = \sprintf( 'Constructor %s::%s() cannot be static', \implode('\\', \array_merge($this->namespace, (array) $className)), $constructor->name ); throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getLine()); } if (\method_exists($constructor, 'getReturnType') && $constructor->getReturnType()) { // For PHP Parser 4.x $className = $classNode->name instanceof Identifier ? $classNode->name->toString() : $classNode->name; $msg = \sprintf( 'Constructor %s::%s() cannot declare a return type', \implode('\\', \array_merge($this->namespace, (array) $className)), $constructor->name ); throw new FatalErrorException($msg, 0, \E_ERROR, null, $classNode->getLine()); } } } �������������������������������������������������������������������������CodeCleaner/FunctionContextPass.php�����������������������������������������������������������������0000644�����������������00000003033�15025056514�0013404 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\FunctionLike; use Psy\Exception\FatalErrorException; class FunctionContextPass extends CodeCleanerPass { /** @var int */ private $functionDepth; /** * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->functionDepth = 0; } /** * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth++; return; } // node is inside function context if ($this->functionDepth !== 0) { return; } // It causes fatal error. if ($node instanceof Yield_) { $msg = 'The "yield" expression can only be used inside a function'; throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } /** * @param \PhpParser\Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth--; } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/ReturnTypePass.php����������������������������������������������������������������������0000644�����������������00000007340�15025056514�0012400 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Identifier; use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\UnionType; use Psy\Exception\FatalErrorException; /** * Add runtime validation for return types. */ class ReturnTypePass extends CodeCleanerPass { const MESSAGE = 'A function with return type must return a value'; const NULLABLE_MESSAGE = 'A function with return type must return a value (did you mean "return null;" instead of "return;"?)'; const VOID_MESSAGE = 'A void function must not return a value'; const VOID_NULL_MESSAGE = 'A void function must not return a value (did you mean "return;" instead of "return null;"?)'; const NULLABLE_VOID_MESSAGE = 'Void type cannot be nullable'; private $atLeastPhp71; private $returnTypeStack = []; public function __construct() { $this->atLeastPhp71 = \version_compare(\PHP_VERSION, '7.1', '>='); } /** * {@inheritdoc} * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if (!$this->atLeastPhp71) { return; // @codeCoverageIgnore } if ($this->isFunctionNode($node)) { $this->returnTypeStack[] = $node->returnType; return; } if (!empty($this->returnTypeStack) && $node instanceof Return_) { $expectedType = \end($this->returnTypeStack); if ($expectedType === null) { return; } $msg = null; if ($this->typeName($expectedType) === 'void') { // Void functions if ($expectedType instanceof NullableType) { $msg = self::NULLABLE_VOID_MESSAGE; } elseif ($node->expr instanceof ConstFetch && \strtolower($node->expr->name) === 'null') { $msg = self::VOID_NULL_MESSAGE; } elseif ($node->expr !== null) { $msg = self::VOID_MESSAGE; } } else { // Everything else if ($node->expr === null) { $msg = $expectedType instanceof NullableType ? self::NULLABLE_MESSAGE : self::MESSAGE; } } if ($msg !== null) { throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } } /** * {@inheritdoc} * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if (!$this->atLeastPhp71) { return; // @codeCoverageIgnore } if (!empty($this->returnTypeStack) && $this->isFunctionNode($node)) { \array_pop($this->returnTypeStack); } } private function isFunctionNode(Node $node): bool { return $node instanceof Function_ || $node instanceof Closure; } private function typeName(Node $node): string { if ($node instanceof UnionType) { return \implode('|', \array_map([$this, 'typeName'], $node->types)); } if ($node instanceof NullableType) { return \strtolower($node->type->name); } if ($node instanceof Identifier) { return \strtolower($node->name); } throw new \InvalidArgumentException('Unable to find type name'); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/NamespacePass.php�����������������������������������������������������������������������0000644�����������������00000004665�15025056514�0012162 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Namespace_; use Psy\CodeCleaner; /** * Provide implicit namespaces for subsequent execution. * * The namespace pass remembers the last standalone namespace line encountered: * * namespace Foo\Bar; * * ... which it then applies implicitly to all future evaluated code, until the * namespace is replaced by another namespace. To reset to the top level * namespace, enter `namespace {}`. This is a bit ugly, but it does the trick :) */ class NamespacePass extends CodeCleanerPass { private $namespace = null; private $cleaner; /** * @param CodeCleaner $cleaner */ public function __construct(CodeCleaner $cleaner) { $this->cleaner = $cleaner; } /** * If this is a standalone namespace line, remember it for later. * * Otherwise, apply remembered namespaces to the code until a new namespace * is encountered. * * @param array $nodes * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { if (empty($nodes)) { return $nodes; } $last = \end($nodes); if ($last instanceof Namespace_) { $kind = $last->getAttribute('kind'); // Treat all namespace statements pre-PHP-Parser v3.1.2 as "open", // even though we really have no way of knowing. if ($kind === null || $kind === Namespace_::KIND_SEMICOLON) { // Save the current namespace for open namespaces $this->setNamespace($last->name); } else { // Clear the current namespace after a braced namespace $this->setNamespace(null); } return $nodes; } return $this->namespace ? [new Namespace_($this->namespace, $nodes)] : $nodes; } /** * Remember the namespace and (re)set the namespace on the CodeCleaner as * well. * * @param Name|null $namespace */ private function setNamespace($namespace) { $this->namespace = $namespace; $this->cleaner->setNamespace($namespace === null ? null : $namespace->parts); } } ���������������������������������������������������������������������������CodeCleaner/UseStatementPass.php��������������������������������������������������������������������0000644�����������������00000010641�15025056514�0012676 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\Stmt\UseUse; use PhpParser\NodeTraverser; /** * Provide implicit use statements for subsequent execution. * * The use statement pass remembers the last use statement line encountered: * * use Foo\Bar as Baz; * * ... which it then applies implicitly to all future evaluated code, until the * current namespace is replaced by another namespace. */ class UseStatementPass extends CodeCleanerPass { private $aliases = []; private $lastAliases = []; private $lastNamespace = null; /** * Re-load the last set of use statements on re-entering a namespace. * * This isn't how namespaces normally work, but because PsySH has to spin * up a new namespace for every line of code, we do this to make things * work like you'd expect. * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Namespace_) { // If this is the same namespace as last namespace, let's do ourselves // a favor and reload all the aliases... if (\strtolower($node->name ?: '') === \strtolower($this->lastNamespace ?: '')) { $this->aliases = $this->lastAliases; } } } /** * If this statement is a namespace, forget all the aliases we had. * * If it's a use statement, remember the alias for later. Otherwise, apply * remembered aliases to the code. * * @param Node $node * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { // Store a reference to every "use" statement, because we'll need them in a bit. if ($node instanceof Use_) { foreach ($node->uses as $use) { $alias = $use->alias ?: \end($use->name->parts); $this->aliases[\strtolower($alias)] = $use->name; } return NodeTraverser::REMOVE_NODE; } // Expand every "use" statement in the group into a full, standalone "use" and store 'em with the others. if ($node instanceof GroupUse) { foreach ($node->uses as $use) { $alias = $use->alias ?: \end($use->name->parts); $this->aliases[\strtolower($alias)] = Name::concat($node->prefix, $use->name, [ 'startLine' => $node->prefix->getAttribute('startLine'), 'endLine' => $use->name->getAttribute('endLine'), ]); } return NodeTraverser::REMOVE_NODE; } // Start fresh, since we're done with this namespace. if ($node instanceof Namespace_) { $this->lastNamespace = $node->name; $this->lastAliases = $this->aliases; $this->aliases = []; return; } // Do nothing with UseUse; this an entry in the list of uses in the use statement. if ($node instanceof UseUse) { return; } // For everything else, we'll implicitly thunk all aliases into fully-qualified names. foreach ($node as $name => $subNode) { if ($subNode instanceof Name) { if ($replacement = $this->findAlias($subNode)) { $node->$name = $replacement; } } } return $node; } /** * Find class/namespace aliases. * * @param Name $name * * @return FullyQualifiedName|null */ private function findAlias(Name $name) { $that = \strtolower($name); foreach ($this->aliases as $alias => $prefix) { if ($that === $alias) { return new FullyQualifiedName($prefix->toString()); } elseif (\substr($that, 0, \strlen($alias) + 1) === $alias.'\\') { return new FullyQualifiedName($prefix->toString().\substr($name, \strlen($alias))); } } } } �����������������������������������������������������������������������������������������������CodeCleaner/RequirePass.php�������������������������������������������������������������������������0000644�����������������00000011071�15025056514�0011667 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Include_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Scalar\LNumber; use Psy\Exception\ErrorException; use Psy\Exception\FatalErrorException; /** * Add runtime validation for `require` and `require_once` calls. */ class RequirePass extends CodeCleanerPass { private static $requireTypes = [Include_::TYPE_REQUIRE, Include_::TYPE_REQUIRE_ONCE]; /** * {@inheritdoc} * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $origNode) { if (!$this->isRequireNode($origNode)) { return; } $node = clone $origNode; /* * rewrite * * $foo = require $bar * * to * * $foo = require \Psy\CodeCleaner\RequirePass::resolve($bar) */ $node->expr = new StaticCall( new FullyQualifiedName(self::class), 'resolve', [new Arg($origNode->expr), new Arg(new LNumber($origNode->getLine()))], $origNode->getAttributes() ); return $node; } /** * Runtime validation that $file can be resolved as an include path. * * If $file can be resolved, return $file. Otherwise throw a fatal error exception. * * If $file collides with a path in the currently running PsySH phar, it will be resolved * relative to the include path, to prevent PHP from grabbing the phar version of the file. * * @throws FatalErrorException when unable to resolve include path for $file * @throws ErrorException if $file is empty and E_WARNING is included in error_reporting level * * @param string $file * @param int $lineNumber Line number of the original require expression * * @return string Exactly the same as $file, unless $file collides with a path in the currently running phar */ public static function resolve($file, $lineNumber = null): string { $file = (string) $file; if ($file === '') { // @todo Shell::handleError would be better here, because we could // fake the file and line number, but we can't call it statically. // So we're duplicating some of the logics here. if (\E_WARNING & \error_reporting()) { ErrorException::throwException(\E_WARNING, 'Filename cannot be empty', null, $lineNumber); } // @todo trigger an error as fallback? this is pretty ugly… // trigger_error('Filename cannot be empty', E_USER_WARNING); } $resolvedPath = \stream_resolve_include_path($file); if ($file === '' || !$resolvedPath) { $msg = \sprintf("Failed opening required '%s'", $file); throw new FatalErrorException($msg, 0, \E_ERROR, null, $lineNumber); } // Special case: if the path is not already relative or absolute, and it would resolve to // something inside the currently running phar (e.g. `vendor/autoload.php`), we'll resolve // it relative to the include path so PHP won't grab the phar version. // // Note that this only works if the phar has `psysh` in the path. We might want to lift this // restriction and special case paths that would collide with any running phar? if ($resolvedPath !== $file && $file[0] !== '.') { $runningPhar = \Phar::running(); if (\strpos($runningPhar, 'psysh') !== false && \is_file($runningPhar.\DIRECTORY_SEPARATOR.$file)) { foreach (self::getIncludePath() as $prefix) { $resolvedPath = $prefix.\DIRECTORY_SEPARATOR.$file; if (\is_file($resolvedPath)) { return $resolvedPath; } } } } return $file; } private function isRequireNode(Node $node): bool { return $node instanceof Include_ && \in_array($node->type, self::$requireTypes); } private static function getIncludePath(): array { if (\PATH_SEPARATOR === ':') { return \preg_split('#:(?!//)#', \get_include_path()); } return \explode(\PATH_SEPARATOR, \get_include_path()); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������CodeCleaner/FunctionReturnInWriteContextPass.php����������������������������������������������������0000644�����������������00000005257�15025056514�0016120 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\CodeCleaner; use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\VariadicPlaceholder; use Psy\Exception\FatalErrorException; /** * Validate that the functions are used correctly. * * @author Martin Hasoň <martin.hason@gmail.com> */ class FunctionReturnInWriteContextPass extends CodeCleanerPass { const ISSET_MESSAGE = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)'; const EXCEPTION_MESSAGE = "Can't use function return value in write context"; /** * Validate that the functions are used correctly. * * @throws FatalErrorException if a function is passed as an argument reference * @throws FatalErrorException if a function is used as an argument in the isset * @throws FatalErrorException if a value is assigned to a function * * @param Node $node * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof Array_ || $this->isCallNode($node)) { $items = $node instanceof Array_ ? $node->items : $node->args; foreach ($items as $item) { if ($item instanceof VariadicPlaceholder) { continue; } if ($item && $item->byRef && $this->isCallNode($item->value)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } } } elseif ($node instanceof Isset_ || $node instanceof Unset_) { foreach ($node->vars as $var) { if (!$this->isCallNode($var)) { continue; } $msg = $node instanceof Isset_ ? self::ISSET_MESSAGE : self::EXCEPTION_MESSAGE; throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getLine()); } } elseif ($node instanceof Assign && $this->isCallNode($node->var)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getLine()); } } private function isCallNode(Node $node): bool { return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������EnvInterface.php������������������������������������������������������������������������������������0000644�����������������00000000673�15025056514�0007637 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy; /** * Abstraction around environment variables. */ interface EnvInterface { /** * Get an environment variable by name. * * @return string|null */ public function get(string $key); } ���������������������������������������������������������������������VersionUpdater/Installer.php������������������������������������������������������������������������0000644�����������������00000007442�15025056514�0012176 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; use Psy\Exception\ErrorException; class Installer { /** * @var string */ protected $installLocation; /** * @var string */ protected $tempDirectory; public function __construct(string $tempDirectory = null) { $this->tempDirectory = $tempDirectory ?: \sys_get_temp_dir(); $this->installLocation = \Phar::running(false); } /** * Public to allow the Downloader to use the temporary directory if it's been set. */ public function getTempDirectory(): string { return $this->tempDirectory; } /** * Verify the currently installed PsySH phar is writable so it can be replaced. */ public function isInstallLocationWritable(): bool { return \is_writable($this->installLocation); } /** * Verify the temporary directory is writable so downloads and backups can be saved there. */ public function isTempDirectoryWritable(): bool { return \is_writable($this->tempDirectory); } /** * Verifies the downloaded archive can be extracted with \PharData. * * @param string $sourceArchive */ public function isValidSource(string $sourceArchive): bool { if (!\class_exists('\PharData')) { return false; } $pharArchive = new \PharData($sourceArchive); return $pharArchive->valid(); } /** * Extract the "psysh" phar from the archive and move it, replacing the currently installed phar. * * @param string $sourceArchive */ public function install(string $sourceArchive): bool { $pharArchive = new \PharData($sourceArchive); $outputDirectory = \tempnam($this->tempDirectory, 'psysh-'); // remove the temp file, and replace it with a sub-directory if (!\unlink($outputDirectory) || !\mkdir($outputDirectory, 0700)) { return false; } $pharArchive->extractTo($outputDirectory, ['psysh'], true); $renamed = \rename($outputDirectory.'/psysh', $this->installLocation); // Remove the sub-directory created to extract the psysh binary/phar \rmdir($outputDirectory); return $renamed; } /** * Create a backup of the currently installed PsySH phar in the temporary directory with a version number postfix. * * @param string $version */ public function createBackup(string $version): bool { $backupFilename = $this->getBackupFilename($version); if (\file_exists($backupFilename) && !\is_writable($backupFilename)) { return false; } return \rename($this->installLocation, $backupFilename); } /** * Restore the backup file to the original PsySH install location. * * @param string $version * * @throws ErrorException If the backup file could not be found */ public function restoreFromBackup(string $version): bool { $backupFilename = $this->getBackupFilename($version); if (!\file_exists($backupFilename)) { throw new ErrorException("Cannot restore from backup. File not found! [{$backupFilename}]"); } return \rename($backupFilename, $this->installLocation); } /** * Get the full path for the backup target file location. * * @param string $version */ public function getBackupFilename(string $version): string { $installFilename = \basename($this->installLocation); return \sprintf('%s/%s.%s', $this->tempDirectory, $installFilename, $version); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/IntervalChecker.php������������������������������������������������������������������0000644�����������������00000003642�15025056514�0013310 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; class IntervalChecker extends GitHubChecker { private $cacheFile; private $interval; public function __construct($cacheFile, $interval) { $this->cacheFile = $cacheFile; $this->interval = $interval; } public function fetchLatestRelease() { // Read the cached file $cached = \json_decode(@\file_get_contents($this->cacheFile, false)); if ($cached && isset($cached->last_check) && isset($cached->release)) { $now = new \DateTime(); $lastCheck = new \DateTime($cached->last_check); if ($lastCheck >= $now->sub($this->getDateInterval())) { return $cached->release; } } // Fall back to fetching from GitHub $release = parent::fetchLatestRelease(); if ($release && isset($release->tag_name)) { $this->updateCache($release); } return $release; } /** * @throws \RuntimeException if interval passed to constructor is not supported */ private function getDateInterval(): \DateInterval { switch ($this->interval) { case Checker::DAILY: return new \DateInterval('P1D'); case Checker::WEEKLY: return new \DateInterval('P1W'); case Checker::MONTHLY: return new \DateInterval('P1M'); } throw new \RuntimeException('Invalid interval configured'); } private function updateCache($release) { $data = [ 'last_check' => \date(\DATE_ATOM), 'release' => $release, ]; \file_put_contents($this->cacheFile, \json_encode($data)); } } ����������������������������������������������������������������������������������������������VersionUpdater/SelfUpdate.php�����������������������������������������������������������������������0000644�����������������00000013276�15025056514�0012277 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; use Psy\Exception\ErrorException; use Psy\Shell; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Self update command. * * If a new version is available, this command will download it and replace the currently installed version */ class SelfUpdate { const URL_PREFIX = 'https://github.com/bobthecow/psysh/releases/download'; const SUCCESS = 0; const FAILURE = 1; /** @var Checker */ private $checker; /** @var Installer */ private $installer; /** @var Downloader */ private $downloader; public function __construct(Checker $checker, Installer $installer) { $this->checker = $checker; $this->installer = $installer; } /** * Allow the downloader to be injected for testing. * * @param Downloader $downloader * * @return void */ public function setDownloader(Downloader $downloader) { $this->downloader = $downloader; } /** * Get the currently set Downloader or create one based on the capabilities of the php environment. * * @throws ErrorException if a downloader cannot be created for the php environment */ private function getDownloader(): Downloader { if (!isset($this->downloader)) { return Downloader\Factory::getDownloader(); } return $this->downloader; } /** * Build the download URL for the latest release. * * The file name used in the URL will include the flavour postfix extracted from the current version * if it's present * * @param string $latestVersion */ private function getAssetUrl(string $latestVersion): string { $versionPostfix = ''; if (\strpos(Shell::VERSION, '+')) { $versionPostfix = '-'.\substr(Shell::VERSION, \strpos(Shell::VERSION, '+') + 1); } $downloadFilename = \sprintf('psysh-%s%s.tar.gz', $latestVersion, $versionPostfix); // check if latest release data contains an asset matching the filename? return \sprintf('%s/%s/%s', self::URL_PREFIX, $latestVersion, $downloadFilename); } /** * Execute the self-update process. * * @param InputInterface $input * @param OutputInterface $output * * @throws ErrorException if the current version is not restored when installation fails */ public function run(InputInterface $input, OutputInterface $output): int { $currentVersion = Shell::VERSION; // already have the latest version? if ($this->checker->isLatest()) { // current version is latest version... $output->writeln('<info>Current version is up-to-date.</info>'); return self::SUCCESS; } // can overwrite current version? if (!$this->installer->isInstallLocationWritable()) { $output->writeln('<error>Installed version is not writable.</error>'); return self::FAILURE; } // can download to, and create a backup in the temp directory? if (!$this->installer->isTempDirectoryWritable()) { $output->writeln('<error>Temporary directory is not writable.</error>'); return self::FAILURE; } $latestVersion = $this->checker->getLatest(); $downloadUrl = $this->getAssetUrl($latestVersion); $output->write("Downloading PsySH $latestVersion ..."); try { $downloader = $this->getDownloader(); $downloader->setTempDir($this->installer->getTempDirectory()); $downloaded = $downloader->download($downloadUrl); } catch (ErrorException $e) { $output->write(' <error>Failed.</error>'); $output->writeln(\sprintf('<error>%s</error>', $e->getMessage())); return self::FAILURE; } if (!$downloaded) { $output->writeln('<error>Download failed.</error>'); $downloader->cleanup(); return self::FAILURE; } else { $output->write(' <info>OK</info>'.\PHP_EOL); } $downloadedFile = $downloader->getFilename(); if (!$this->installer->isValidSource($downloadedFile)) { $downloader->cleanup(); $output->writeln('<error>Downloaded file is not a valid archive.</error>'); return self::FAILURE; } // create backup as bin.old-version in the temporary directory $backupCreated = $this->installer->createBackup($currentVersion); if (!$backupCreated) { $downloader->cleanup(); $output->writeln('<error>Failed to create a backup of the current version.</error>'); return self::FAILURE; } elseif ($input->getOption('verbose')) { $backupFilename = $this->installer->getBackupFilename($currentVersion); $output->writeln('Created backup of current version: '.$backupFilename); } if (!$this->installer->install($downloadedFile)) { $this->installer->restoreFromBackup($currentVersion); $downloader->cleanup(); $output->writeln("<error>Failed to install new PsySH version $latestVersion.</error>"); return self::FAILURE; } // Remove the downloaded archive file from the temporary directory $downloader->cleanup(); $output->writeln("Updated PsySH from $currentVersion to <info>$latestVersion</info>"); return self::SUCCESS; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/Downloader.php�����������������������������������������������������������������������0000644�����������������00000001533�15025056514�0012332 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; use Psy\Exception\ErrorException; interface Downloader { /** * Set the directory where the download will be written to. * * @param string $tempDir */ public function setTempDir(string $tempDir); /** * @param string $url * * @throws ErrorException on failure */ public function download(string $url): bool; /** * Get the temporary file name the download was written to. */ public function getFilename(): string; /** * Delete the downloaded file if it exists. * * @return void */ public function cleanup(); } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/GitHubChecker.php��������������������������������������������������������������������0000644�����������������00000004033�15025056514�0012701 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; use Psy\Shell; class GitHubChecker implements Checker { const URL = 'https://api.github.com/repos/bobthecow/psysh/releases/latest'; private $latest; public function isLatest(): bool { // version_compare doesn't handle semver completely; // strip pre-release and build metadata before comparing $version = \preg_replace('/[+-]\w+/', '', Shell::VERSION); return \version_compare($version, $this->getLatest(), '>='); } public function getLatest(): string { if (!isset($this->latest)) { $this->setLatest($this->getVersionFromTag()); } return $this->latest; } /** * @param string $version */ public function setLatest(string $version) { $this->latest = $version; } /** * @return string|null */ private function getVersionFromTag() { $contents = $this->fetchLatestRelease(); if (!$contents || !isset($contents->tag_name)) { throw new \InvalidArgumentException('Unable to check for updates'); } $this->setLatest($contents->tag_name); return $this->getLatest(); } /** * Set to public to make testing easier. * * @return mixed */ public function fetchLatestRelease() { $context = \stream_context_create([ 'http' => [ 'user_agent' => 'PsySH/'.Shell::VERSION, 'timeout' => 1.0, ], ]); \set_error_handler(function () { // Just ignore all errors with this. The checker will throw an exception // if it doesn't work :) }); $result = @\file_get_contents(self::URL, false, $context); \restore_error_handler(); return \json_decode($result); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/Downloader/FileDownloader.php��������������������������������������������������������0000644�����������������00000002405�15025056514�0015227 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater\Downloader; use Psy\VersionUpdater\Downloader; class FileDownloader implements Downloader { private $tempDir = null; private $outputFile = null; /** {@inheritDoc} */ public function setTempDir(string $tempDir) { $this->tempDir = $tempDir; } /** {@inheritDoc} */ public function download(string $url): bool { $tempDir = $this->tempDir ?: \sys_get_temp_dir(); $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); $targetName = $this->outputFile.'.tar.gz'; if (!\rename($this->outputFile, $targetName)) { return false; } $this->outputFile = $targetName; return (bool) \file_put_contents($this->outputFile, \file_get_contents($url)); } /** {@inheritDoc} */ public function getFilename(): string { return $this->outputFile; } /** {@inheritDoc} */ public function cleanup() { if (\file_exists($this->outputFile)) { \unlink($this->outputFile); } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/Downloader/CurlDownloader.php��������������������������������������������������������0000644�����������������00000004065�15025056514�0015261 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater\Downloader; use Psy\Exception\ErrorException; use Psy\Shell; use Psy\VersionUpdater\Downloader; class CurlDownloader implements Downloader { private $tempDir = null; private $outputFile = null; /** {@inheritDoc} */ public function setTempDir(string $tempDir) { $this->tempDir = $tempDir; } /** {@inheritDoc} */ public function download(string $url): bool { $tempDir = $this->tempDir ?: \sys_get_temp_dir(); $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); $targetName = $this->outputFile.'.tar.gz'; if (!\rename($this->outputFile, $targetName)) { return false; } $this->outputFile = $targetName; $outputHandle = \fopen($this->outputFile, 'w'); if (!$outputHandle) { return false; } $curl = \curl_init(); \curl_setopt_array($curl, [ \CURLOPT_FAILONERROR => true, \CURLOPT_HEADER => 0, \CURLOPT_FOLLOWLOCATION => true, \CURLOPT_TIMEOUT => 10, \CURLOPT_FILE => $outputHandle, \CURLOPT_HTTPHEADER => [ 'User-Agent' => 'PsySH/'.Shell::VERSION, ], ]); \curl_setopt($curl, \CURLOPT_URL, $url); $result = \curl_exec($curl); $error = \curl_error($curl); \curl_close($curl); \fclose($outputHandle); if (!$result) { throw new ErrorException('cURL Error: '.$error); } return (bool) $result; } /** {@inheritDoc} */ public function getFilename(): string { return $this->outputFile; } /** {@inheritDoc} */ public function cleanup() { if (\file_exists($this->outputFile)) { \unlink($this->outputFile); } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/Downloader/Factory.php���������������������������������������������������������������0000644�����������������00000001334�15025056514�0013740 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater\Downloader; use Psy\Exception\ErrorException; use Psy\VersionUpdater\Downloader; class Factory { /** * @throws ErrorException If no downloaders can be used */ public static function getDownloader(): Downloader { if (\extension_loaded('curl')) { return new CurlDownloader(); } elseif (\ini_get('allow_url_fopen')) { return new FileDownloader(); } throw new ErrorException('No downloader available.'); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������VersionUpdater/Checker.php��������������������������������������������������������������������������0000644�����������������00000000754�15025056514�0011604 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; interface Checker { const ALWAYS = 'always'; const DAILY = 'daily'; const WEEKLY = 'weekly'; const MONTHLY = 'monthly'; const NEVER = 'never'; public function isLatest(): bool; public function getLatest(): string; } ��������������������VersionUpdater/NoopChecker.php����������������������������������������������������������������������0000644�����������������00000001042�15025056514�0012427 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\VersionUpdater; use Psy\Shell; /** * A version checker stub which always thinks the current version is up to date. */ class NoopChecker implements Checker { public function isLatest(): bool { return true; } public function getLatest(): string { return Shell::VERSION; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/Schema.php�����������������������������������������������������������������������������������0000644�����������������00000001062�15025056515�0007660 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; interface Schema { /** * Normalization. * @return mixed */ function normalize($value, Context $context); /** * Merging. * @return mixed */ function merge($value, $base); /** * Validation and finalization. * @return mixed */ function complete($value, Context $context); /** * @return mixed */ function completeDefault(Context $context); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/DynamicParameter.php�������������������������������������������������������������������������0000644�����������������00000000336�15025056515�0011710 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; interface DynamicParameter { } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/Context.php����������������������������������������������������������������������������������0000644�����������������00000001637�15025056515�0010114 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; final class Context { use Nette\SmartObject; /** @var bool */ public $skipDefaults = false; /** @var string[] */ public $path = []; /** @var bool */ public $isKey = false; /** @var Message[] */ public $errors = []; /** @var Message[] */ public $warnings = []; /** @var array[] */ public $dynamics = []; public function addError(string $message, string $code, array $variables = []): Message { $variables['isKey'] = $this->isKey; return $this->errors[] = new Message($message, $code, $this->path, $variables); } public function addWarning(string $message, string $code, array $variables = []): Message { return $this->warnings[] = new Message($message, $code, $this->path, $variables); } } �������������������������������������������������������������������������������������������������Schema/Message.php����������������������������������������������������������������������������������0000644�����������������00000003573�15025056515�0010055 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; final class Message { use Nette\SmartObject; /** variables: {value: mixed, expected: string} */ public const TYPE_MISMATCH = 'schema.typeMismatch'; /** variables: {value: mixed, expected: string} */ public const VALUE_OUT_OF_RANGE = 'schema.valueOutOfRange'; /** variables: {value: mixed, length: int, expected: string} */ public const LENGTH_OUT_OF_RANGE = 'schema.lengthOutOfRange'; /** variables: {value: string, pattern: string} */ public const PATTERN_MISMATCH = 'schema.patternMismatch'; /** variables: {value: mixed, assertion: string} */ public const FAILED_ASSERTION = 'schema.failedAssertion'; /** no variables */ public const MISSING_ITEM = 'schema.missingItem'; /** variables: {hint: string} */ public const UNEXPECTED_ITEM = 'schema.unexpectedItem'; /** no variables */ public const DEPRECATED = 'schema.deprecated'; /** @var string */ public $message; /** @var string */ public $code; /** @var string[] */ public $path; /** @var string[] */ public $variables; public function __construct(string $message, string $code, array $path, array $variables = []) { $this->message = $message; $this->code = $code; $this->path = $path; $this->variables = $variables; } public function toString(): string { $vars = $this->variables; $vars['label'] = empty($vars['isKey']) ? 'item' : 'key of item'; $vars['path'] = $this->path ? "'" . implode("\u{a0}›\u{a0}", $this->path) . "'" : null; $vars['value'] = Helpers::formatValue($vars['value'] ?? null); return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) { [, $space, $key] = $m; return $vars[$key] === null ? '' : $space . $vars[$key]; }, $this->message); } } �������������������������������������������������������������������������������������������������������������������������������������Schema/Helpers.php����������������������������������������������������������������������������������0000644�����������������00000004757�15025056515�0010100 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; use Nette\Utils\Reflection; /** * @internal */ final class Helpers { use Nette\StaticClass; public const PREVENT_MERGING = '_prevent_merging'; /** * Merges dataset. Left has higher priority than right one. * @return array|string */ public static function merge($value, $base) { if (is_array($value) && isset($value[self::PREVENT_MERGING])) { unset($value[self::PREVENT_MERGING]); return $value; } if (is_array($value) && is_array($base)) { $index = 0; foreach ($value as $key => $val) { if ($key === $index) { $base[] = $val; $index++; } else { $base[$key] = static::merge($val, $base[$key] ?? null); } } return $base; } elseif ($value === null && is_array($base)) { return $base; } else { return $value; } } public static function getPropertyType(\ReflectionProperty $prop): ?string { if (!class_exists(Nette\Utils\Type::class)) { throw new Nette\NotSupportedException('Expect::from() requires nette/utils 3.x'); } elseif ($type = Nette\Utils\Type::fromReflection($prop)) { return (string) $type; } elseif ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) { $class = Reflection::getPropertyDeclaringClass($prop); return preg_replace_callback('#[\w\\\\]+#', function ($m) use ($class) { return Reflection::expandClassName($m[0], $class); }, $type); } return null; } /** * Returns an annotation value. * @param \ReflectionProperty $ref */ public static function parseAnnotation(\Reflector $ref, string $name): ?string { if (!Reflection::areCommentsAvailable()) { throw new Nette\InvalidStateException('You have to enable phpDoc comments in opcode cache.'); } $re = '#[\s*]@' . preg_quote($name, '#') . '(?=\s|$)(?:[ \t]+([^@\s]\S*))?#'; if ($ref->getDocComment() && preg_match($re, trim($ref->getDocComment(), '/*'), $m)) { return $m[1] ?? ''; } return null; } /** * @param mixed $value */ public static function formatValue($value): string { if (is_object($value)) { return 'object ' . get_class($value); } elseif (is_string($value)) { return "'" . Nette\Utils\Strings::truncate($value, 15, '...') . "'"; } elseif (is_scalar($value)) { return var_export($value, true); } else { return strtolower(gettype($value)); } } } �����������������Schema/ValidationException.php����������������������������������������������������������������������0000644�����������������00000001521�15025056515�0012431 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; /** * Validation error. */ class ValidationException extends Nette\InvalidStateException { /** @var Message[] */ private $messages; /** * @param Message[] $messages */ public function __construct(?string $message, array $messages = []) { parent::__construct($message ?: $messages[0]->toString()); $this->messages = $messages; } /** * @return string[] */ public function getMessages(): array { $res = []; foreach ($this->messages as $message) { $res[] = $message->toString(); } return $res; } /** * @return Message[] */ public function getMessageObjects(): array { return $this->messages; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/Expect.php�����������������������������������������������������������������������������������0000644�����������������00000004723�15025056515�0007717 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; use Nette\Schema\Elements\AnyOf; use Nette\Schema\Elements\Structure; use Nette\Schema\Elements\Type; /** * Schema generator. * * @method static Type scalar($default = null) * @method static Type string($default = null) * @method static Type int($default = null) * @method static Type float($default = null) * @method static Type bool($default = null) * @method static Type null() * @method static Type array($default = []) * @method static Type list($default = []) * @method static Type mixed($default = null) * @method static Type email($default = null) * @method static Type unicode($default = null) */ final class Expect { use Nette\SmartObject; public static function __callStatic(string $name, array $args): Type { $type = new Type($name); if ($args) { $type->default($args[0]); } return $type; } public static function type(string $type): Type { return new Type($type); } /** * @param mixed|Schema ...$set */ public static function anyOf(...$set): AnyOf { return new AnyOf(...$set); } /** * @param Schema[] $items */ public static function structure(array $items): Structure { return new Structure($items); } /** * @param object $object */ public static function from($object, array $items = []): Structure { $ro = new \ReflectionObject($object); foreach ($ro->getProperties() as $prop) { $type = Helpers::getPropertyType($prop) ?? 'mixed'; $item = &$items[$prop->getName()]; if (!$item) { $item = new Type($type); if (PHP_VERSION_ID >= 70400 && !$prop->isInitialized($object)) { $item->required(); } else { $def = $prop->getValue($object); if (is_object($def)) { $item = static::from($def); } elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) { $item->required(); } else { $item->default($def); } } } } return (new Structure($items))->castTo($ro->getName()); } /** * @param string|Schema $valueType * @param string|Schema|null $keyType */ public static function arrayOf($valueType, $keyType = null): Type { return (new Type('array'))->items($valueType, $keyType); } /** * @param string|Schema $type */ public static function listOf($type): Type { return (new Type('list'))->items($type); } } ���������������������������������������������Schema/Processor.php��������������������������������������������������������������������������������0000644�����������������00000003674�15025056515�0010452 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema; use Nette; /** * Schema validator. */ final class Processor { use Nette\SmartObject; /** @var array */ public $onNewContext = []; /** @var Context|null */ private $context; /** @var bool */ private $skipDefaults; public function skipDefaults(bool $value = true) { $this->skipDefaults = $value; } /** * Normalizes and validates data. Result is a clean completed data. * @return mixed * @throws ValidationException */ public function process(Schema $schema, $data) { $this->createContext(); $data = $schema->normalize($data, $this->context); $this->throwsErrors(); $data = $schema->complete($data, $this->context); $this->throwsErrors(); return $data; } /** * Normalizes and validates and merges multiple data. Result is a clean completed data. * @return mixed * @throws ValidationException */ public function processMultiple(Schema $schema, array $dataset) { $this->createContext(); $flatten = null; $first = true; foreach ($dataset as $data) { $data = $schema->normalize($data, $this->context); $this->throwsErrors(); $flatten = $first ? $data : $schema->merge($data, $flatten); $first = false; } $data = $schema->complete($flatten, $this->context); $this->throwsErrors(); return $data; } /** * @return string[] */ public function getWarnings(): array { $res = []; foreach ($this->context->warnings as $message) { $res[] = $message->toString(); } return $res; } private function throwsErrors(): void { if ($this->context->errors) { throw new ValidationException(null, $this->context->errors); } } private function createContext() { $this->context = new Context; $this->context->skipDefaults = $this->skipDefaults; $this->onNewContext($this->context); } } ��������������������������������������������������������������������Schema/Elements/Structure.php�����������������������������������������������������������������������0000644�����������������00000010516�15025056515�0012240 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema\Elements; use Nette; use Nette\Schema\Context; use Nette\Schema\Helpers; use Nette\Schema\Schema; final class Structure implements Schema { use Base; use Nette\SmartObject; /** @var Schema[] */ private $items; /** @var Schema|null for array|list */ private $otherItems; /** @var array{?int, ?int} */ private $range = [null, null]; /** @var bool */ private $skipDefaults = false; /** * @param Schema[] $items */ public function __construct(array $items) { (function (Schema ...$items) {})(...array_values($items)); $this->items = $items; $this->castTo = 'object'; $this->required = true; } public function default($value): self { throw new Nette\InvalidStateException('Structure cannot have default value.'); } public function min(?int $min): self { $this->range[0] = $min; return $this; } public function max(?int $max): self { $this->range[1] = $max; return $this; } /** * @param string|Schema $type */ public function otherItems($type = 'mixed'): self { $this->otherItems = $type instanceof Schema ? $type : new Type($type); return $this; } public function skipDefaults(bool $state = true): self { $this->skipDefaults = $state; return $this; } /********************* processing ****************d*g**/ public function normalize($value, Context $context) { if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) { unset($value[Helpers::PREVENT_MERGING]); } $value = $this->doNormalize($value, $context); if (is_object($value)) { $value = (array) $value; } if (is_array($value)) { foreach ($value as $key => $val) { $itemSchema = $this->items[$key] ?? $this->otherItems; if ($itemSchema) { $context->path[] = $key; $value[$key] = $itemSchema->normalize($val, $context); array_pop($context->path); } } if ($prevent) { $value[Helpers::PREVENT_MERGING] = true; } } return $value; } public function merge($value, $base) { if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { unset($value[Helpers::PREVENT_MERGING]); $base = null; } if (is_array($value) && is_array($base)) { $index = 0; foreach ($value as $key => $val) { if ($key === $index) { $base[] = $val; $index++; } elseif (array_key_exists($key, $base)) { $itemSchema = $this->items[$key] ?? $this->otherItems; $base[$key] = $itemSchema ? $itemSchema->merge($val, $base[$key]) : Helpers::merge($val, $base[$key]); } else { $base[$key] = $val; } } return $base; } return Helpers::merge($value, $base); } public function complete($value, Context $context) { if ($value === null) { $value = []; // is unable to distinguish null from array in NEON } $this->doDeprecation($context); if (!$this->doValidate($value, 'array', $context) || !$this->doValidateRange($value, $this->range, $context) ) { return; } $errCount = count($context->errors); $items = $this->items; if ($extraKeys = array_keys(array_diff_key($value, $items))) { if ($this->otherItems) { $items += array_fill_keys($extraKeys, $this->otherItems); } else { $keys = array_map('strval', array_keys($items)); foreach ($extraKeys as $key) { $hint = Nette\Utils\ObjectHelpers::getSuggestion($keys, (string) $key); $context->addError( 'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'), Nette\Schema\Message::UNEXPECTED_ITEM, ['hint' => $hint] )->path[] = $key; } } } foreach ($items as $itemKey => $itemVal) { $context->path[] = $itemKey; if (array_key_exists($itemKey, $value)) { $value[$itemKey] = $itemVal->complete($value[$itemKey], $context); } else { $default = $itemVal->completeDefault($context); // checks required item if (!$context->skipDefaults && !$this->skipDefaults) { $value[$itemKey] = $default; } } array_pop($context->path); } if (count($context->errors) > $errCount) { return; } return $this->doFinalize($value, $context); } public function completeDefault(Context $context) { return $this->required ? $this->complete([], $context) : null; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/Elements/Type.php����������������������������������������������������������������������������0000644�����������������00000011610�15025056515�0011155 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema\Elements; use Nette; use Nette\Schema\Context; use Nette\Schema\DynamicParameter; use Nette\Schema\Helpers; use Nette\Schema\Schema; final class Type implements Schema { use Base; use Nette\SmartObject; /** @var string */ private $type; /** @var Schema|null for arrays */ private $itemsValue; /** @var Schema|null for arrays */ private $itemsKey; /** @var array{?float, ?float} */ private $range = [null, null]; /** @var string|null */ private $pattern; /** @var bool */ private $merge = true; public function __construct(string $type) { $defaults = ['list' => [], 'array' => []]; $this->type = $type; $this->default = strpos($type, '[]') ? [] : $defaults[$type] ?? null; } public function nullable(): self { $this->type = 'null|' . $this->type; return $this; } public function mergeDefaults(bool $state = true): self { $this->merge = $state; return $this; } public function dynamic(): self { $this->type = DynamicParameter::class . '|' . $this->type; return $this; } public function min(?float $min): self { $this->range[0] = $min; return $this; } public function max(?float $max): self { $this->range[1] = $max; return $this; } /** * @param string|Schema $valueType * @param string|Schema|null $keyType * @internal use arrayOf() or listOf() */ public function items($valueType = 'mixed', $keyType = null): self { $this->itemsValue = $valueType instanceof Schema ? $valueType : new self($valueType); $this->itemsKey = $keyType instanceof Schema || $keyType === null ? $keyType : new self($keyType); return $this; } public function pattern(?string $pattern): self { $this->pattern = $pattern; return $this; } /********************* processing ****************d*g**/ public function normalize($value, Context $context) { if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) { unset($value[Helpers::PREVENT_MERGING]); } $value = $this->doNormalize($value, $context); if (is_array($value) && $this->itemsValue) { $res = []; foreach ($value as $key => $val) { $context->path[] = $key; $context->isKey = true; $key = $this->itemsKey ? $this->itemsKey->normalize($key, $context) : $key; $context->isKey = false; $res[$key] = $this->itemsValue->normalize($val, $context); array_pop($context->path); } $value = $res; } if ($prevent && is_array($value)) { $value[Helpers::PREVENT_MERGING] = true; } return $value; } public function merge($value, $base) { if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { unset($value[Helpers::PREVENT_MERGING]); return $value; } if (is_array($value) && is_array($base) && $this->itemsValue) { $index = 0; foreach ($value as $key => $val) { if ($key === $index) { $base[] = $val; $index++; } else { $base[$key] = array_key_exists($key, $base) ? $this->itemsValue->merge($val, $base[$key]) : $val; } } return $base; } return Helpers::merge($value, $base); } public function complete($value, Context $context) { $merge = $this->merge; if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { unset($value[Helpers::PREVENT_MERGING]); $merge = false; } if ($value === null && is_array($this->default)) { $value = []; // is unable to distinguish null from array in NEON } $this->doDeprecation($context); if (!$this->doValidate($value, $this->type, $context) || !$this->doValidateRange($value, $this->range, $context, $this->type) ) { return; } if ($value !== null && $this->pattern !== null && !preg_match("\x01^(?:$this->pattern)$\x01Du", $value)) { $context->addError( "The %label% %path% expects to match pattern '%pattern%', %value% given.", Nette\Schema\Message::PATTERN_MISMATCH, ['value' => $value, 'pattern' => $this->pattern] ); return; } if ($value instanceof DynamicParameter) { $expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range)); $context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected)]; } if ($this->itemsValue) { $errCount = count($context->errors); $res = []; foreach ($value as $key => $val) { $context->path[] = $key; $context->isKey = true; $key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key; $context->isKey = false; $res[$key] = $this->itemsValue->complete($val, $context); array_pop($context->path); } if (count($context->errors) > $errCount) { return null; } $value = $res; } if ($merge) { $value = Helpers::merge($value, $this->default); } return $this->doFinalize($value, $context); } } ������������������������������������������������������������������������������������������������������������������������Schema/Elements/AnyOf.php���������������������������������������������������������������������������0000644�����������������00000005421�15025056515�0011253 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema\Elements; use Nette; use Nette\Schema\Context; use Nette\Schema\Helpers; use Nette\Schema\Schema; final class AnyOf implements Schema { use Base; use Nette\SmartObject; /** @var array */ private $set; /** * @param mixed|Schema ...$set */ public function __construct(...$set) { if (!$set) { throw new Nette\InvalidStateException('The enumeration must not be empty.'); } $this->set = $set; } public function firstIsDefault(): self { $this->default = $this->set[0]; return $this; } public function nullable(): self { $this->set[] = null; return $this; } public function dynamic(): self { $this->set[] = new Type(Nette\Schema\DynamicParameter::class); return $this; } /********************* processing ****************d*g**/ public function normalize($value, Context $context) { return $this->doNormalize($value, $context); } public function merge($value, $base) { if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) { unset($value[Helpers::PREVENT_MERGING]); return $value; } return Helpers::merge($value, $base); } public function complete($value, Context $context) { $expecteds = $innerErrors = []; foreach ($this->set as $item) { if ($item instanceof Schema) { $dolly = new Context; $dolly->path = $context->path; $res = $item->complete($item->normalize($value, $dolly), $dolly); if (!$dolly->errors) { $context->warnings = array_merge($context->warnings, $dolly->warnings); return $this->doFinalize($res, $context); } foreach ($dolly->errors as $error) { if ($error->path !== $context->path || empty($error->variables['expected'])) { $innerErrors[] = $error; } else { $expecteds[] = $error->variables['expected']; } } } else { if ($item === $value) { return $this->doFinalize($value, $context); } $expecteds[] = Nette\Schema\Helpers::formatValue($item); } } if ($innerErrors) { $context->errors = array_merge($context->errors, $innerErrors); } else { $context->addError( 'The %label% %path% expects to be %expected%, %value% given.', Nette\Schema\Message::TYPE_MISMATCH, [ 'value' => $value, 'expected' => implode('|', array_unique($expecteds)), ] ); } } public function completeDefault(Context $context) { if ($this->required) { $context->addError( 'The mandatory item %path% is missing.', Nette\Schema\Message::MISSING_ITEM ); return null; } if ($this->default instanceof Schema) { return $this->default->completeDefault($context); } return $this->default; } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Schema/Elements/Base.php����������������������������������������������������������������������������0000644�����������������00000010246�15025056515�0011112 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * This file is part of the Nette Framework (https://nette.org) * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ declare(strict_types=1); namespace Nette\Schema\Elements; use Nette; use Nette\Schema\Context; /** * @internal */ trait Base { /** @var bool */ private $required = false; /** @var mixed */ private $default; /** @var callable|null */ private $before; /** @var array[] */ private $asserts = []; /** @var string|null */ private $castTo; /** @var string|null */ private $deprecated; public function default($value): self { $this->default = $value; return $this; } public function required(bool $state = true): self { $this->required = $state; return $this; } public function before(callable $handler): self { $this->before = $handler; return $this; } public function castTo(string $type): self { $this->castTo = $type; return $this; } public function assert(callable $handler, ?string $description = null): self { $this->asserts[] = [$handler, $description]; return $this; } /** Marks as deprecated */ public function deprecated(string $message = 'The item %path% is deprecated.'): self { $this->deprecated = $message; return $this; } public function completeDefault(Context $context) { if ($this->required) { $context->addError( 'The mandatory item %path% is missing.', Nette\Schema\Message::MISSING_ITEM ); return null; } return $this->default; } public function doNormalize($value, Context $context) { if ($this->before) { $value = ($this->before)($value); } return $value; } private function doDeprecation(Context $context): void { if ($this->deprecated !== null) { $context->addWarning( $this->deprecated, Nette\Schema\Message::DEPRECATED ); } } private function doValidate($value, string $expected, Context $context): bool { if (!Nette\Utils\Validators::is($value, $expected)) { $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); $context->addError( 'The %label% %path% expects to be %expected%, %value% given.', Nette\Schema\Message::TYPE_MISMATCH, ['value' => $value, 'expected' => $expected] ); return false; } return true; } private function doValidateRange($value, array $range, Context $context, string $types = ''): bool { if (is_array($value) || is_string($value)) { [$length, $label] = is_array($value) ? [count($value), 'items'] : (in_array('unicode', explode('|', $types), true) ? [Nette\Utils\Strings::length($value), 'characters'] : [strlen($value), 'bytes']); if (!self::isInRange($length, $range)) { $context->addError( "The length of %label% %path% expects to be in range %expected%, %length% $label given.", Nette\Schema\Message::LENGTH_OUT_OF_RANGE, ['value' => $value, 'length' => $length, 'expected' => implode('..', $range)] ); return false; } } elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) { $context->addError( 'The %label% %path% expects to be in range %expected%, %value% given.', Nette\Schema\Message::VALUE_OUT_OF_RANGE, ['value' => $value, 'expected' => implode('..', $range)] ); return false; } return true; } private function isInRange($value, array $range): bool { return ($range[0] === null || $value >= $range[0]) && ($range[1] === null || $value <= $range[1]); } private function doFinalize($value, Context $context) { if ($this->castTo) { if (Nette\Utils\Reflection::isBuiltinType($this->castTo)) { settype($value, $this->castTo); } else { $object = new $this->castTo; foreach ($value as $k => $v) { $object->$k = $v; } $value = $object; } } foreach ($this->asserts as $i => [$handler, $description]) { if (!$handler($value)) { $expected = $description ?: (is_string($handler) ? "$handler()" : "#$i"); $context->addError( 'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.', Nette\Schema\Message::FAILED_ASSERTION, ['value' => $value, 'assertion' => $expected] ); return; } } return $value; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������LoggerTrait.php�������������������������������������������������������������������������������������0000644�����������������00000007167�15025104553�0007513 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * This is a simple Logger trait that classes unable to extend AbstractLogger * (because they extend another class, etc) can include. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ trait LoggerTrait { /** * System is unusable. * * @param string|\Stringable $message * @param array $context * * @return void */ public function emergency(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string|\Stringable $message * @param array $context * * @return void */ public function alert(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string|\Stringable $message * @param array $context * * @return void */ public function critical(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string|\Stringable $message * @param array $context * * @return void */ public function error(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string|\Stringable $message * @param array $context * * @return void */ public function warning(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string|\Stringable $message * @param array $context * * @return void */ public function notice(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string|\Stringable $message * @param array $context * * @return void */ public function info(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string|\Stringable $message * @param array $context * * @return void */ public function debug(string|\Stringable $message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string|\Stringable $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ abstract public function log($level, string|\Stringable $message, array $context = []): void; } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������NullLogger.php��������������������������������������������������������������������������������������0000644�����������������00000001342�15025104553�0007327 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * This Logger can be used to avoid conditional log calls. * * Logging should always be optional, and if no logger is provided to your * library creating a NullLogger instance to have something to throw logs at * is a good way to avoid littering your code with `if ($this->logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string|\Stringable $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, string|\Stringable $message, array $context = []): void { // noop } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������LoggerAwareInterface.php����������������������������������������������������������������������������0000644�����������������00000000457�15025104553�0011303 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * Describes a logger-aware instance. */ interface LoggerAwareInterface { /** * Sets a logger instance on the object. * * @param LoggerInterface $logger * * @return void */ public function setLogger(LoggerInterface $logger): void; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������LogLevel.php����������������������������������������������������������������������������������������0000644�����������������00000000520�15025104553�0006763 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * Describes log levels. */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������InvalidArgumentException.php������������������������������������������������������������������������0000644�����������������00000000140�15025104553�0012220 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; class InvalidArgumentException extends \InvalidArgumentException { } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������LoggerInterface.php���������������������������������������������������������������������������������0000644�����������������00000006501�15025104553�0010317 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * Describes a logger instance. * * The message MUST be a string or object implementing __toString(). * * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data. The only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md * for the full interface specification. */ interface LoggerInterface { /** * System is unusable. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function emergency(string|\Stringable $message, array $context = []): void; /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function alert(string|\Stringable $message, array $context = []): void; /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function critical(string|\Stringable $message, array $context = []): void; /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function error(string|\Stringable $message, array $context = []): void; /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function warning(string|\Stringable $message, array $context = []): void; /** * Normal but significant events. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function notice(string|\Stringable $message, array $context = []): void; /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function info(string|\Stringable $message, array $context = []): void; /** * Detailed debug information. * * @param string|\Stringable $message * @param mixed[] $context * * @return void */ public function debug(string|\Stringable $message, array $context = []): void; /** * Logs with an arbitrary level. * * @param mixed $level * @param string|\Stringable $message * @param mixed[] $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, string|\Stringable $message, array $context = []): void; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������AbstractLogger.php����������������������������������������������������������������������������������0000644�����������������00000000636�15025104553�0010165 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * This is a simple Logger implementation that other Loggers can inherit from. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ abstract class AbstractLogger implements LoggerInterface { use LoggerTrait; } ��������������������������������������������������������������������������������������������������LoggerAwareTrait.php��������������������������������������������������������������������������������0000644�����������������00000000660�15025104553�0010462 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php namespace Psr\Log; /** * Basic Implementation of LoggerAwareInterface. */ trait LoggerAwareTrait { /** * The logger instance. * * @var LoggerInterface|null */ protected ?LoggerInterface $logger = null; /** * Sets a logger. * * @param LoggerInterface $logger */ public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } } ��������������������������������������������������������������������������������Whoops/Inspector/InspectorInterface.php�������������������������������������������������������������0000644�����������������00000003036�15025130165�0014271 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Inspector; interface InspectorInterface { /** * @return \Throwable */ public function getException(); /** * @return string */ public function getExceptionName(); /** * @return string */ public function getExceptionMessage(); /** * @return string[] */ public function getPreviousExceptionMessages(); /** * @return int[] */ public function getPreviousExceptionCodes(); /** * Returns a url to the php-manual related to the underlying error - when available. * * @return string|null */ public function getExceptionDocrefUrl(); /** * Does the wrapped Exception has a previous Exception? * @return bool */ public function hasPreviousException(); /** * Returns an Inspector for a previous Exception, if any. * @todo Clean this up a bit, cache stuff a bit better. * @return InspectorInterface */ public function getPreviousExceptionInspector(); /** * Returns an array of all previous exceptions for this inspector's exception * @return \Throwable[] */ public function getPreviousExceptions(); /** * Returns an iterator for the inspected exception's * frames. * * @param array<callable> $frameFilters * * @return \Whoops\Exception\FrameCollection */ public function getFrames(array $frameFilters = []); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Inspector/InspectorFactoryInterface.php������������������������������������������������������0000644�����������������00000000453�15025130165�0015621 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Inspector; interface InspectorFactoryInterface { /** * @param \Throwable $exception * @return InspectorInterface */ public function create($exception); } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Inspector/InspectorFactory.php���������������������������������������������������������������0000644�����������������00000000640�15025130165�0013776 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Inspector; use Whoops\Exception\Inspector; class InspectorFactory implements InspectorFactoryInterface { /** * @param \Throwable $exception * @return InspectorInterface */ public function create($exception) { return new Inspector($exception, $this); } } ������������������������������������������������������������������������������������������������Whoops/Util/Misc.php��������������������������������������������������������������������������������0000644�����������������00000003703�15025130165�0010345 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Util; class Misc { /** * Can we at this point in time send HTTP headers? * * Currently this checks if we are even serving an HTTP request, * as opposed to running from a command line. * * If we are serving an HTTP request, we check if it's not too late. * * @return bool */ public static function canSendHeaders() { return isset($_SERVER["REQUEST_URI"]) && !headers_sent(); } public static function isAjaxRequest() { return ( !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); } /** * Check, if possible, that this execution was triggered by a command line. * @return bool */ public static function isCommandLine() { return PHP_SAPI == 'cli'; } /** * Translate ErrorException code into the represented constant. * * @param int $error_code * @return string */ public static function translateErrorCode($error_code) { $constants = get_defined_constants(true); if (array_key_exists('Core', $constants)) { foreach ($constants['Core'] as $constant => $value) { if (substr($constant, 0, 2) == 'E_' && $value == $error_code) { return $constant; } } } return "E_UNKNOWN"; } /** * Determine if an error level is fatal (halts execution) * * @param int $level * @return bool */ public static function isLevelFatal($level) { $errors = E_ERROR; $errors |= E_PARSE; $errors |= E_CORE_ERROR; $errors |= E_CORE_WARNING; $errors |= E_COMPILE_ERROR; $errors |= E_COMPILE_WARNING; return ($level & $errors) > 0; } } �������������������������������������������������������������Whoops/Util/SystemFacade.php������������������������������������������������������������������������0000644�����������������00000005217�15025130165�0012024 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Util; class SystemFacade { /** * Turns on output buffering. * * @return bool */ public function startOutputBuffering() { return ob_start(); } /** * @param callable $handler * @param int $types * * @return callable|null */ public function setErrorHandler(callable $handler, $types = 'use-php-defaults') { // Since PHP 5.4 the constant E_ALL contains all errors (even E_STRICT) if ($types === 'use-php-defaults') { $types = E_ALL; } return set_error_handler($handler, $types); } /** * @param callable $handler * * @return callable|null */ public function setExceptionHandler(callable $handler) { return set_exception_handler($handler); } /** * @return void */ public function restoreExceptionHandler() { restore_exception_handler(); } /** * @return void */ public function restoreErrorHandler() { restore_error_handler(); } /** * @param callable $function * * @return void */ public function registerShutdownFunction(callable $function) { register_shutdown_function($function); } /** * @return string|false */ public function cleanOutputBuffer() { return ob_get_clean(); } /** * @return int */ public function getOutputBufferLevel() { return ob_get_level(); } /** * @return bool */ public function endOutputBuffering() { return ob_end_clean(); } /** * @return void */ public function flushOutputBuffer() { flush(); } /** * @return int */ public function getErrorReportingLevel() { return error_reporting(); } /** * @return array|null */ public function getLastError() { return error_get_last(); } /** * @param int $httpCode * * @return int */ public function setHttpResponseCode($httpCode) { if (!headers_sent()) { // Ensure that no 'location' header is present as otherwise this // will override the HTTP code being set here, and mask the // expected error page. header_remove('location'); } return http_response_code($httpCode); } /** * @param int $exitStatus */ public function stopExecution($exitStatus) { exit($exitStatus); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Util/HtmlDumperOutput.php��������������������������������������������������������������������0000644�����������������00000001316�15025130165�0012752 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Util; /** * Used as output callable for Symfony\Component\VarDumper\Dumper\HtmlDumper::dump() * * @see TemplateHelper::dump() */ class HtmlDumperOutput { private $output; public function __invoke($line, $depth) { // A negative depth means "end of dump" if ($depth >= 0) { // Adds a two spaces indentation to the line $this->output .= str_repeat(' ', $depth) . $line . "\n"; } } public function getOutput() { return $this->output; } public function clear() { $this->output = null; } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Util/TemplateHelper.php����������������������������������������������������������������������0000644�����������������00000022403�15025130165�0012363 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Util; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Cloner\AbstractCloner; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Whoops\Exception\Frame; /** * Exposes useful tools for working with/in templates */ class TemplateHelper { /** * An array of variables to be passed to all templates * @var array */ private $variables = []; /** * @var HtmlDumper */ private $htmlDumper; /** * @var HtmlDumperOutput */ private $htmlDumperOutput; /** * @var AbstractCloner */ private $cloner; /** * @var string */ private $applicationRootPath; public function __construct() { // root path for ordinary composer projects $this->applicationRootPath = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))); } /** * Escapes a string for output in an HTML document * * @param string $raw * @return string */ public function escape($raw) { $flags = ENT_QUOTES; // HHVM has all constants defined, but only ENT_IGNORE // works at the moment if (defined("ENT_SUBSTITUTE") && !defined("HHVM_VERSION")) { $flags |= ENT_SUBSTITUTE; } else { // This is for 5.3. // The documentation warns of a potential security issue, // but it seems it does not apply in our case, because // we do not blacklist anything anywhere. $flags |= ENT_IGNORE; } $raw = str_replace(chr(9), ' ', $raw); return htmlspecialchars($raw, $flags, "UTF-8"); } /** * Escapes a string for output in an HTML document, but preserves * URIs within it, and converts them to clickable anchor elements. * * @param string $raw * @return string */ public function escapeButPreserveUris($raw) { $escaped = $this->escape($raw); return preg_replace( "@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@", "<a href=\"$1\" target=\"_blank\" rel=\"noreferrer noopener\">$1</a>", $escaped ); } /** * Makes sure that the given string breaks on the delimiter. * * @param string $delimiter * @param string $s * @return string */ public function breakOnDelimiter($delimiter, $s) { $parts = explode($delimiter, $s); foreach ($parts as &$part) { $part = '<span class="delimiter">' . $part . '</span>'; } return implode($delimiter, $parts); } /** * Replace the part of the path that all files have in common. * * @param string $path * @return string */ public function shorten($path) { if ($this->applicationRootPath != "/") { $path = str_replace($this->applicationRootPath, '…', $path); } return $path; } private function getDumper() { if (!$this->htmlDumper && class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { $this->htmlDumperOutput = new HtmlDumperOutput(); // re-use the same var-dumper instance, so it won't re-render the global styles/scripts on each dump. $this->htmlDumper = new HtmlDumper($this->htmlDumperOutput); $styles = [ 'default' => 'color:#FFFFFF; line-height:normal; font:12px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace !important; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal', 'num' => 'color:#BCD42A', 'const' => 'color: #4bb1b1;', 'str' => 'color:#BCD42A', 'note' => 'color:#ef7c61', 'ref' => 'color:#A0A0A0', 'public' => 'color:#FFFFFF', 'protected' => 'color:#FFFFFF', 'private' => 'color:#FFFFFF', 'meta' => 'color:#FFFFFF', 'key' => 'color:#BCD42A', 'index' => 'color:#ef7c61', ]; $this->htmlDumper->setStyles($styles); } return $this->htmlDumper; } /** * Format the given value into a human readable string. * * @param mixed $value * @return string */ public function dump($value) { $dumper = $this->getDumper(); if ($dumper) { // re-use the same DumpOutput instance, so it won't re-render the global styles/scripts on each dump. // exclude verbose information (e.g. exception stack traces) if (class_exists('Symfony\Component\VarDumper\Caster\Caster')) { $cloneVar = $this->getCloner()->cloneVar($value, Caster::EXCLUDE_VERBOSE); // Symfony VarDumper 2.6 Caster class dont exist. } else { $cloneVar = $this->getCloner()->cloneVar($value); } $dumper->dump( $cloneVar, $this->htmlDumperOutput ); $output = $this->htmlDumperOutput->getOutput(); $this->htmlDumperOutput->clear(); return $output; } return htmlspecialchars(print_r($value, true)); } /** * Format the args of the given Frame as a human readable html string * * @param Frame $frame * @return string the rendered html */ public function dumpArgs(Frame $frame) { // we support frame args only when the optional dumper is available if (!$this->getDumper()) { return ''; } $html = ''; $numFrames = count($frame->getArgs()); if ($numFrames > 0) { $html = '<ol class="linenums">'; foreach ($frame->getArgs() as $j => $frameArg) { $html .= '<li>'. $this->dump($frameArg) .'</li>'; } $html .= '</ol>'; } return $html; } /** * Convert a string to a slug version of itself * * @param string $original * @return string */ public function slug($original) { $slug = str_replace(" ", "-", $original); $slug = preg_replace('/[^\w\d\-\_]/i', '', $slug); return strtolower($slug); } /** * Given a template path, render it within its own scope. This * method also accepts an array of additional variables to be * passed to the template. * * @param string $template */ public function render($template, array $additionalVariables = null) { $variables = $this->getVariables(); // Pass the helper to the template: $variables["tpl"] = $this; if ($additionalVariables !== null) { $variables = array_replace($variables, $additionalVariables); } call_user_func(function () { extract(func_get_arg(1)); require func_get_arg(0); }, $template, $variables); } /** * Sets the variables to be passed to all templates rendered * by this template helper. */ public function setVariables(array $variables) { $this->variables = $variables; } /** * Sets a single template variable, by its name: * * @param string $variableName * @param mixed $variableValue */ public function setVariable($variableName, $variableValue) { $this->variables[$variableName] = $variableValue; } /** * Gets a single template variable, by its name, or * $defaultValue if the variable does not exist * * @param string $variableName * @param mixed $defaultValue * @return mixed */ public function getVariable($variableName, $defaultValue = null) { return isset($this->variables[$variableName]) ? $this->variables[$variableName] : $defaultValue; } /** * Unsets a single template variable, by its name * * @param string $variableName */ public function delVariable($variableName) { unset($this->variables[$variableName]); } /** * Returns all variables for this helper * * @return array */ public function getVariables() { return $this->variables; } /** * Set the cloner used for dumping variables. * * @param AbstractCloner $cloner */ public function setCloner($cloner) { $this->cloner = $cloner; } /** * Get the cloner used for dumping variables. * * @return AbstractCloner */ public function getCloner() { if (!$this->cloner) { $this->cloner = new VarCloner(); } return $this->cloner; } /** * Set the application root path. * * @param string $applicationRootPath */ public function setApplicationRootPath($applicationRootPath) { $this->applicationRootPath = $applicationRootPath; } /** * Return the application root path. * * @return string */ public function getApplicationRootPath() { return $this->applicationRootPath; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Exception/FrameCollection.php����������������������������������������������������������������0000644�����������������00000012051�15025130165�0013535 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Exception; use ArrayAccess; use ArrayIterator; use Countable; use IteratorAggregate; use ReturnTypeWillChange; use Serializable; use UnexpectedValueException; /** * Exposes a fluent interface for dealing with an ordered list * of stack-trace frames. */ class FrameCollection implements ArrayAccess, IteratorAggregate, Serializable, Countable { /** * @var array[] */ private $frames; public function __construct(array $frames) { $this->frames = array_map(function ($frame) { return new Frame($frame); }, $frames); } /** * Filters frames using a callable, returns the same FrameCollection * * @param callable $callable * @return FrameCollection */ public function filter($callable) { $this->frames = array_values(array_filter($this->frames, $callable)); return $this; } /** * Map the collection of frames * * @param callable $callable * @return FrameCollection */ public function map($callable) { // Contain the map within a higher-order callable // that enforces type-correctness for the $callable $this->frames = array_map(function ($frame) use ($callable) { $frame = call_user_func($callable, $frame); if (!$frame instanceof Frame) { throw new UnexpectedValueException( "Callable to " . __CLASS__ . "::map must return a Frame object" ); } return $frame; }, $this->frames); return $this; } /** * Returns an array with all frames, does not affect * the internal array. * * @todo If this gets any more complex than this, * have getIterator use this method. * @see FrameCollection::getIterator * @return array */ public function getArray() { return $this->frames; } /** * @see IteratorAggregate::getIterator * @return ArrayIterator */ #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->frames); } /** * @see ArrayAccess::offsetExists * @param int $offset */ #[ReturnTypeWillChange] public function offsetExists($offset) { return isset($this->frames[$offset]); } /** * @see ArrayAccess::offsetGet * @param int $offset */ #[ReturnTypeWillChange] public function offsetGet($offset) { return $this->frames[$offset]; } /** * @see ArrayAccess::offsetSet * @param int $offset */ #[ReturnTypeWillChange] public function offsetSet($offset, $value) { throw new \Exception(__CLASS__ . ' is read only'); } /** * @see ArrayAccess::offsetUnset * @param int $offset */ #[ReturnTypeWillChange] public function offsetUnset($offset) { throw new \Exception(__CLASS__ . ' is read only'); } /** * @see Countable::count * @return int */ #[ReturnTypeWillChange] public function count() { return count($this->frames); } /** * Count the frames that belongs to the application. * * @return int */ public function countIsApplication() { return count(array_filter($this->frames, function (Frame $f) { return $f->isApplication(); })); } /** * @see Serializable::serialize * @return string */ #[ReturnTypeWillChange] public function serialize() { return serialize($this->frames); } /** * @see Serializable::unserialize * @param string $serializedFrames */ #[ReturnTypeWillChange] public function unserialize($serializedFrames) { $this->frames = unserialize($serializedFrames); } public function __serialize() { return $this->frames; } public function __unserialize(array $serializedFrames) { $this->frames = $serializedFrames; } /** * @param Frame[] $frames Array of Frame instances, usually from $e->getPrevious() */ public function prependFrames(array $frames) { $this->frames = array_merge($frames, $this->frames); } /** * Gets the innermost part of stack trace that is not the same as that of outer exception * * @param FrameCollection $parentFrames Outer exception frames to compare tail against * @return Frame[] */ public function topDiff(FrameCollection $parentFrames) { $diff = $this->frames; $parentFrames = $parentFrames->getArray(); $p = count($parentFrames)-1; for ($i = count($diff)-1; $i >= 0 && $p >= 0; $i--) { /** @var Frame $tailFrame */ $tailFrame = $diff[$i]; if ($tailFrame->equals($parentFrames[$p])) { unset($diff[$i]); } $p--; } return $diff; } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Exception/Inspector.php����������������������������������������������������������������������0000644�����������������00000022735�15025130165�0012447 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Exception; use Whoops\Inspector\InspectorFactory; use Whoops\Inspector\InspectorInterface; use Whoops\Util\Misc; class Inspector implements InspectorInterface { /** * @var \Throwable */ private $exception; /** * @var \Whoops\Exception\FrameCollection */ private $frames; /** * @var \Whoops\Exception\Inspector */ private $previousExceptionInspector; /** * @var \Throwable[] */ private $previousExceptions; /** * @var \Whoops\Inspector\InspectorFactoryInterface|null */ protected $inspectorFactory; /** * @param \Throwable $exception The exception to inspect * @param \Whoops\Inspector\InspectorFactoryInterface $factory */ public function __construct($exception, $factory = null) { $this->exception = $exception; $this->inspectorFactory = $factory ?: new InspectorFactory(); } /** * @return \Throwable */ public function getException() { return $this->exception; } /** * @return string */ public function getExceptionName() { return get_class($this->exception); } /** * @return string */ public function getExceptionMessage() { return $this->extractDocrefUrl($this->exception->getMessage())['message']; } /** * @return string[] */ public function getPreviousExceptionMessages() { return array_map(function ($prev) { /** @var \Throwable $prev */ return $this->extractDocrefUrl($prev->getMessage())['message']; }, $this->getPreviousExceptions()); } /** * @return int[] */ public function getPreviousExceptionCodes() { return array_map(function ($prev) { /** @var \Throwable $prev */ return $prev->getCode(); }, $this->getPreviousExceptions()); } /** * Returns a url to the php-manual related to the underlying error - when available. * * @return string|null */ public function getExceptionDocrefUrl() { return $this->extractDocrefUrl($this->exception->getMessage())['url']; } private function extractDocrefUrl($message) { $docref = [ 'message' => $message, 'url' => null, ]; // php embbeds urls to the manual into the Exception message with the following ini-settings defined // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root if (!ini_get('html_errors') || !ini_get('docref_root')) { return $docref; } $pattern = "/\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/"; if (preg_match($pattern, $message, $matches)) { // -> strip those automatically generated links from the exception message $docref['message'] = preg_replace($pattern, '', $message, 1); $docref['url'] = $matches[1]; } return $docref; } /** * Does the wrapped Exception has a previous Exception? * @return bool */ public function hasPreviousException() { return $this->previousExceptionInspector || $this->exception->getPrevious(); } /** * Returns an Inspector for a previous Exception, if any. * @todo Clean this up a bit, cache stuff a bit better. * @return Inspector */ public function getPreviousExceptionInspector() { if ($this->previousExceptionInspector === null) { $previousException = $this->exception->getPrevious(); if ($previousException) { $this->previousExceptionInspector = $this->inspectorFactory->create($previousException); } } return $this->previousExceptionInspector; } /** * Returns an array of all previous exceptions for this inspector's exception * @return \Throwable[] */ public function getPreviousExceptions() { if ($this->previousExceptions === null) { $this->previousExceptions = []; $prev = $this->exception->getPrevious(); while ($prev !== null) { $this->previousExceptions[] = $prev; $prev = $prev->getPrevious(); } } return $this->previousExceptions; } /** * Returns an iterator for the inspected exception's * frames. * * @param array<callable> $frameFilters * * @return \Whoops\Exception\FrameCollection */ public function getFrames(array $frameFilters = []) { if ($this->frames === null) { $frames = $this->getTrace($this->exception); // Fill empty line/file info for call_user_func_array usages (PHP Bug #44428) foreach ($frames as $k => $frame) { if (empty($frame['file'])) { // Default values when file and line are missing $file = '[internal]'; $line = 0; $next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : []; if ($this->isValidNextFrame($next_frame)) { $file = $next_frame['file']; $line = $next_frame['line']; } $frames[$k]['file'] = $file; $frames[$k]['line'] = $line; } } // Find latest non-error handling frame index ($i) used to remove error handling frames $i = 0; foreach ($frames as $k => $frame) { if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) { $i = $k; } } // Remove error handling frames if ($i > 0) { array_splice($frames, 0, $i); } $firstFrame = $this->getFrameFromException($this->exception); array_unshift($frames, $firstFrame); $this->frames = new FrameCollection($frames); if ($previousInspector = $this->getPreviousExceptionInspector()) { // Keep outer frame on top of the inner one $outerFrames = $this->frames; $newFrames = clone $previousInspector->getFrames(); // I assume it will always be set, but let's be safe if (isset($newFrames[0])) { $newFrames[0]->addComment( $previousInspector->getExceptionMessage(), 'Exception message:' ); } $newFrames->prependFrames($outerFrames->topDiff($newFrames)); $this->frames = $newFrames; } // Apply frame filters callbacks on the frames stack if (!empty($frameFilters)) { foreach ($frameFilters as $filterCallback) { $this->frames->filter($filterCallback); } } } return $this->frames; } /** * Gets the backtrace from an exception. * * If xdebug is installed * * @param \Throwable $e * @return array */ protected function getTrace($e) { $traces = $e->getTrace(); // Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default if (!$e instanceof \ErrorException) { return $traces; } if (!Misc::isLevelFatal($e->getSeverity())) { return $traces; } if (!extension_loaded('xdebug') || !function_exists('xdebug_is_enabled') || !xdebug_is_enabled()) { return $traces; } // Use xdebug to get the full stack trace and remove the shutdown handler stack trace $stack = array_reverse(xdebug_get_function_stack()); $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $traces = array_diff_key($stack, $trace); return $traces; } /** * Given an exception, generates an array in the format * generated by Exception::getTrace() * @param \Throwable $exception * @return array */ protected function getFrameFromException($exception) { return [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'class' => get_class($exception), 'args' => [ $exception->getMessage(), ], ]; } /** * Given an error, generates an array in the format * generated by ErrorException * @param ErrorException $exception * @return array */ protected function getFrameFromError(ErrorException $exception) { return [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'class' => null, 'args' => [], ]; } /** * Determine if the frame can be used to fill in previous frame's missing info * happens for call_user_func and call_user_func_array usages (PHP Bug #44428) * * @return bool */ protected function isValidNextFrame(array $frame) { if (empty($frame['file'])) { return false; } if (empty($frame['line'])) { return false; } if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) { return false; } return true; } } �����������������������������������Whoops/Exception/ErrorException.php�����������������������������������������������������������������0000644�����������������00000000543�15025130165�0013442 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Exception; use ErrorException as BaseErrorException; /** * Wraps ErrorException; mostly used for typing (at least now) * to easily cleanup the stack trace of redundant info. */ class ErrorException extends BaseErrorException { } �������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Exception/Formatter.php����������������������������������������������������������������������0000644�����������������00000004632�15025130165�0012440 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Exception; use Whoops\Inspector\InspectorInterface; class Formatter { /** * Returns all basic information about the exception in a simple array * for further convertion to other languages * @param InspectorInterface $inspector * @param bool $shouldAddTrace * @param array<callable> $frameFilters * @return array */ public static function formatExceptionAsDataArray(InspectorInterface $inspector, $shouldAddTrace, array $frameFilters = []) { $exception = $inspector->getException(); $response = [ 'type' => get_class($exception), 'message' => $exception->getMessage(), 'code' => $exception->getCode(), 'file' => $exception->getFile(), 'line' => $exception->getLine(), ]; if ($shouldAddTrace) { $frames = $inspector->getFrames($frameFilters); $frameData = []; foreach ($frames as $frame) { /** @var Frame $frame */ $frameData[] = [ 'file' => $frame->getFile(), 'line' => $frame->getLine(), 'function' => $frame->getFunction(), 'class' => $frame->getClass(), 'args' => $frame->getArgs(), ]; } $response['trace'] = $frameData; } return $response; } public static function formatExceptionPlain(InspectorInterface $inspector) { $message = $inspector->getException()->getMessage(); $frames = $inspector->getFrames(); $plain = $inspector->getExceptionName(); $plain .= ' thrown with message "'; $plain .= $message; $plain .= '"'."\n\n"; $plain .= "Stacktrace:\n"; foreach ($frames as $i => $frame) { $plain .= "#". (count($frames) - $i - 1). " "; $plain .= $frame->getClass() ?: ''; $plain .= $frame->getClass() && $frame->getFunction() ? ":" : ""; $plain .= $frame->getFunction() ?: ''; $plain .= ' in '; $plain .= ($frame->getFile() ?: '<#unknown>'); $plain .= ':'; $plain .= (int) $frame->getLine(). "\n"; } return $plain; } } ������������������������������������������������������������������������������������������������������Whoops/Exception/Frame.php��������������������������������������������������������������������������0000644�����������������00000017761�15025130165�0011536 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Exception; use InvalidArgumentException; use Serializable; class Frame implements Serializable { /** * @var array */ protected $frame; /** * @var string */ protected $fileContentsCache; /** * @var array[] */ protected $comments = []; /** * @var bool */ protected $application; public function __construct(array $frame) { $this->frame = $frame; } /** * @param bool $shortened * @return string|null */ public function getFile($shortened = false) { if (empty($this->frame['file'])) { return null; } $file = $this->frame['file']; // Check if this frame occurred within an eval(). // @todo: This can be made more reliable by checking if we've entered // eval() in a previous trace, but will need some more work on the upper // trace collector(s). if (preg_match('/^(.*)\((\d+)\) : (?:eval\(\)\'d|assert) code$/', $file, $matches)) { $file = $this->frame['file'] = $matches[1]; $this->frame['line'] = (int) $matches[2]; } if ($shortened && is_string($file)) { // Replace the part of the path that all frames have in common, and add 'soft hyphens' for smoother line-breaks. $dirname = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))); if ($dirname !== '/') { $file = str_replace($dirname, "…", $file); } $file = str_replace("/", "/­", $file); } return $file; } /** * @return int|null */ public function getLine() { return isset($this->frame['line']) ? $this->frame['line'] : null; } /** * @return string|null */ public function getClass() { return isset($this->frame['class']) ? $this->frame['class'] : null; } /** * @return string|null */ public function getFunction() { return isset($this->frame['function']) ? $this->frame['function'] : null; } /** * @return array */ public function getArgs() { return isset($this->frame['args']) ? (array) $this->frame['args'] : []; } /** * Returns the full contents of the file for this frame, * if it's known. * @return string|null */ public function getFileContents() { if ($this->fileContentsCache === null && $filePath = $this->getFile()) { // Leave the stage early when 'Unknown' or '[internal]' is passed // this would otherwise raise an exception when // open_basedir is enabled. if ($filePath === "Unknown" || $filePath === '[internal]') { return null; } try { $this->fileContentsCache = file_get_contents($filePath); } catch (ErrorException $exception) { // Internal file paths of PHP extensions cannot be opened } } return $this->fileContentsCache; } /** * Adds a comment to this frame, that can be received and * used by other handlers. For example, the PrettyPage handler * can attach these comments under the code for each frame. * * An interesting use for this would be, for example, code analysis * & annotations. * * @param string $comment * @param string $context Optional string identifying the origin of the comment */ public function addComment($comment, $context = 'global') { $this->comments[] = [ 'comment' => $comment, 'context' => $context, ]; } /** * Returns all comments for this frame. Optionally allows * a filter to only retrieve comments from a specific * context. * * @param string $filter * @return array[] */ public function getComments($filter = null) { $comments = $this->comments; if ($filter !== null) { $comments = array_filter($comments, function ($c) use ($filter) { return $c['context'] == $filter; }); } return $comments; } /** * Returns the array containing the raw frame data from which * this Frame object was built * * @return array */ public function getRawFrame() { return $this->frame; } /** * Returns the contents of the file for this frame as an * array of lines, and optionally as a clamped range of lines. * * NOTE: lines are 0-indexed * * @example * Get all lines for this file * $frame->getFileLines(); // => array( 0 => '<?php', 1 => '...', ...) * @example * Get one line for this file, starting at line 10 (zero-indexed, remember!) * $frame->getFileLines(9, 1); // array( 9 => '...' ) * * @throws InvalidArgumentException if $length is less than or equal to 0 * @param int $start * @param int $length * @return string[]|null */ public function getFileLines($start = 0, $length = null) { if (null !== ($contents = $this->getFileContents())) { $lines = explode("\n", $contents); // Get a subset of lines from $start to $end if ($length !== null) { $start = (int) $start; $length = (int) $length; if ($start < 0) { $start = 0; } if ($length <= 0) { throw new InvalidArgumentException( "\$length($length) cannot be lower or equal to 0" ); } $lines = array_slice($lines, $start, $length, true); } return $lines; } } /** * Implements the Serializable interface, with special * steps to also save the existing comments. * * @see Serializable::serialize * @return string */ public function serialize() { $frame = $this->frame; if (!empty($this->comments)) { $frame['_comments'] = $this->comments; } return serialize($frame); } public function __serialize() { $frame = $this->frame; if (!empty($this->comments)) { $frame['_comments'] = $this->comments; } return $frame; } /** * Unserializes the frame data, while also preserving * any existing comment data. * * @see Serializable::unserialize * @param string $serializedFrame */ public function unserialize($serializedFrame) { $frame = unserialize($serializedFrame); if (!empty($frame['_comments'])) { $this->comments = $frame['_comments']; unset($frame['_comments']); } $this->frame = $frame; } public function __unserialize($frame) { if (!empty($frame['_comments'])) { $this->comments = $frame['_comments']; unset($frame['_comments']); } $this->frame = $frame; } /** * Compares Frame against one another * @param Frame $frame * @return bool */ public function equals(Frame $frame) { if (!$this->getFile() || $this->getFile() === 'Unknown' || !$this->getLine()) { return false; } return $frame->getFile() === $this->getFile() && $frame->getLine() === $this->getLine(); } /** * Returns whether this frame belongs to the application or not. * * @return boolean */ public function isApplication() { return $this->application; } /** * Mark as an frame belonging to the application. * * @param boolean $application */ public function setApplication($application) { $this->application = $application; } } ���������������Whoops/Handler/JsonResponseHandler.php��������������������������������������������������������������0000644�����������������00000004075�15025130165�0014043 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use Whoops\Exception\Formatter; /** * Catches an exception and converts it to a JSON * response. Additionally can also return exception * frames for consumption by an API. */ class JsonResponseHandler extends Handler { /** * @var bool */ private $returnFrames = false; /** * @var bool */ private $jsonApi = false; /** * Returns errors[[]] instead of error[] to be in compliance with the json:api spec * @param bool $jsonApi Default is false * @return static */ public function setJsonApi($jsonApi = false) { $this->jsonApi = (bool) $jsonApi; return $this; } /** * @param bool|null $returnFrames * @return bool|static */ public function addTraceToOutput($returnFrames = null) { if (func_num_args() == 0) { return $this->returnFrames; } $this->returnFrames = (bool) $returnFrames; return $this; } /** * @return int */ public function handle() { if ($this->jsonApi === true) { $response = [ 'errors' => [ Formatter::formatExceptionAsDataArray( $this->getInspector(), $this->addTraceToOutput(), $this->getRun()->getFrameFilters() ), ] ]; } else { $response = [ 'error' => Formatter::formatExceptionAsDataArray( $this->getInspector(), $this->addTraceToOutput(), $this->getRun()->getFrameFilters() ), ]; } echo json_encode($response, defined('JSON_PARTIAL_OUTPUT_ON_ERROR') ? JSON_PARTIAL_OUTPUT_ON_ERROR : 0); return Handler::QUIT; } /** * @return string */ public function contentType() { return 'application/json'; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/CallbackHandler.php������������������������������������������������������������������0000644�����������������00000002510�15025130165�0013077 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use InvalidArgumentException; /** * Wrapper for Closures passed as handlers. Can be used * directly, or will be instantiated automagically by Whoops\Run * if passed to Run::pushHandler */ class CallbackHandler extends Handler { /** * @var callable */ protected $callable; /** * @throws InvalidArgumentException If argument is not callable * @param callable $callable */ public function __construct($callable) { if (!is_callable($callable)) { throw new InvalidArgumentException( 'Argument to ' . __METHOD__ . ' must be valid callable' ); } $this->callable = $callable; } /** * @return int|null */ public function handle() { $exception = $this->getException(); $inspector = $this->getInspector(); $run = $this->getRun(); $callable = $this->callable; // invoke the callable directly, to get simpler stacktraces (in comparison to call_user_func). // this assumes that $callable is a properly typed php-callable, which we check in __construct(). return $callable($exception, $inspector, $run); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/PlainTextHandler.php�����������������������������������������������������������������0000644�����������������00000021413�15025130165�0013316 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> * Plaintext handler for command line and logs. * @author Pierre-Yves Landuré <https://howto.biapy.com/> */ namespace Whoops\Handler; use InvalidArgumentException; use Psr\Log\LoggerInterface; use Whoops\Exception\Frame; /** * Handler outputing plaintext error messages. Can be used * directly, or will be instantiated automagically by Whoops\Run * if passed to Run::pushHandler */ class PlainTextHandler extends Handler { const VAR_DUMP_PREFIX = ' | '; /** * @var \Psr\Log\LoggerInterface */ protected $logger; /** * @var callable */ protected $dumper; /** * @var bool */ private $addTraceToOutput = true; /** * @var bool|integer */ private $addTraceFunctionArgsToOutput = false; /** * @var integer */ private $traceFunctionArgsOutputLimit = 1024; /** * @var bool */ private $addPreviousToOutput = true; /** * @var bool */ private $loggerOnly = false; /** * Constructor. * @throws InvalidArgumentException If argument is not null or a LoggerInterface * @param \Psr\Log\LoggerInterface|null $logger */ public function __construct($logger = null) { $this->setLogger($logger); } /** * Set the output logger interface. * @throws InvalidArgumentException If argument is not null or a LoggerInterface * @param \Psr\Log\LoggerInterface|null $logger */ public function setLogger($logger = null) { if (! (is_null($logger) || $logger instanceof LoggerInterface)) { throw new InvalidArgumentException( 'Argument to ' . __METHOD__ . " must be a valid Logger Interface (aka. Monolog), " . get_class($logger) . ' given.' ); } $this->logger = $logger; } /** * @return \Psr\Log\LoggerInterface|null */ public function getLogger() { return $this->logger; } /** * Set var dumper callback function. * * @param callable $dumper * @return static */ public function setDumper(callable $dumper) { $this->dumper = $dumper; return $this; } /** * Add error trace to output. * @param bool|null $addTraceToOutput * @return bool|static */ public function addTraceToOutput($addTraceToOutput = null) { if (func_num_args() == 0) { return $this->addTraceToOutput; } $this->addTraceToOutput = (bool) $addTraceToOutput; return $this; } /** * Add previous exceptions to output. * @param bool|null $addPreviousToOutput * @return bool|static */ public function addPreviousToOutput($addPreviousToOutput = null) { if (func_num_args() == 0) { return $this->addPreviousToOutput; } $this->addPreviousToOutput = (bool) $addPreviousToOutput; return $this; } /** * Add error trace function arguments to output. * Set to True for all frame args, or integer for the n first frame args. * @param bool|integer|null $addTraceFunctionArgsToOutput * @return static|bool|integer */ public function addTraceFunctionArgsToOutput($addTraceFunctionArgsToOutput = null) { if (func_num_args() == 0) { return $this->addTraceFunctionArgsToOutput; } if (! is_integer($addTraceFunctionArgsToOutput)) { $this->addTraceFunctionArgsToOutput = (bool) $addTraceFunctionArgsToOutput; } else { $this->addTraceFunctionArgsToOutput = $addTraceFunctionArgsToOutput; } return $this; } /** * Set the size limit in bytes of frame arguments var_dump output. * If the limit is reached, the var_dump output is discarded. * Prevent memory limit errors. * @var integer * @return static */ public function setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit) { $this->traceFunctionArgsOutputLimit = (integer) $traceFunctionArgsOutputLimit; return $this; } /** * Create plain text response and return it as a string * @return string */ public function generateResponse() { $exception = $this->getException(); $message = $this->getExceptionOutput($exception); if ($this->addPreviousToOutput) { $previous = $exception->getPrevious(); while ($previous) { $message .= "\n\nCaused by\n" . $this->getExceptionOutput($previous); $previous = $previous->getPrevious(); } } return $message . $this->getTraceOutput() . "\n"; } /** * Get the size limit in bytes of frame arguments var_dump output. * If the limit is reached, the var_dump output is discarded. * Prevent memory limit errors. * @return integer */ public function getTraceFunctionArgsOutputLimit() { return $this->traceFunctionArgsOutputLimit; } /** * Only output to logger. * @param bool|null $loggerOnly * @return static|bool */ public function loggerOnly($loggerOnly = null) { if (func_num_args() == 0) { return $this->loggerOnly; } $this->loggerOnly = (bool) $loggerOnly; return $this; } /** * Test if handler can output to stdout. * @return bool */ private function canOutput() { return !$this->loggerOnly(); } /** * Get the frame args var_dump. * @param \Whoops\Exception\Frame $frame [description] * @param integer $line [description] * @return string */ private function getFrameArgsOutput(Frame $frame, $line) { if ($this->addTraceFunctionArgsToOutput() === false || $this->addTraceFunctionArgsToOutput() < $line) { return ''; } // Dump the arguments: ob_start(); $this->dump($frame->getArgs()); if (ob_get_length() > $this->getTraceFunctionArgsOutputLimit()) { // The argument var_dump is to big. // Discarded to limit memory usage. ob_clean(); return sprintf( "\n%sArguments dump length greater than %d Bytes. Discarded.", self::VAR_DUMP_PREFIX, $this->getTraceFunctionArgsOutputLimit() ); } return sprintf( "\n%s", preg_replace('/^/m', self::VAR_DUMP_PREFIX, ob_get_clean()) ); } /** * Dump variable. * * @param mixed $var * @return void */ protected function dump($var) { if ($this->dumper) { call_user_func($this->dumper, $var); } else { var_dump($var); } } /** * Get the exception trace as plain text. * @return string */ private function getTraceOutput() { if (! $this->addTraceToOutput()) { return ''; } $inspector = $this->getInspector(); $frames = $inspector->getFrames($this->getRun()->getFrameFilters()); $response = "\nStack trace:"; $line = 1; foreach ($frames as $frame) { /** @var Frame $frame */ $class = $frame->getClass(); $template = "\n%3d. %s->%s() %s:%d%s"; if (! $class) { // Remove method arrow (->) from output. $template = "\n%3d. %s%s() %s:%d%s"; } $response .= sprintf( $template, $line, $class, $frame->getFunction(), $frame->getFile(), $frame->getLine(), $this->getFrameArgsOutput($frame, $line) ); $line++; } return $response; } /** * Get the exception as plain text. * @param \Throwable $exception * @return string */ private function getExceptionOutput($exception) { return sprintf( "%s: %s in file %s on line %d", get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine() ); } /** * @return int */ public function handle() { $response = $this->generateResponse(); if ($this->getLogger()) { $this->getLogger()->error($response); } if (! $this->canOutput()) { return Handler::DONE; } echo $response; return Handler::QUIT; } /** * @return string */ public function contentType() { return 'text/plain'; } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/HandlerInterface.php�����������������������������������������������������������������0000644�����������������00000001352�15025130165�0013306 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use Whoops\Inspector\InspectorInterface; use Whoops\RunInterface; interface HandlerInterface { /** * @return int|null A handler may return nothing, or a Handler::HANDLE_* constant */ public function handle(); /** * @param RunInterface $run * @return void */ public function setRun(RunInterface $run); /** * @param \Throwable $exception * @return void */ public function setException($exception); /** * @param InspectorInterface $inspector * @return void */ public function setInspector(InspectorInterface $inspector); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/Handler.php��������������������������������������������������������������������������0000644�����������������00000003572�15025130165�0011473 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use Whoops\Inspector\InspectorInterface; use Whoops\RunInterface; /** * Abstract implementation of a Handler. */ abstract class Handler implements HandlerInterface { /* Return constants that can be returned from Handler::handle to message the handler walker. */ const DONE = 0x10; // returning this is optional, only exists for // semantic purposes /** * The Handler has handled the Throwable in some way, and wishes to skip any other Handler. * Execution will continue. */ const LAST_HANDLER = 0x20; /** * The Handler has handled the Throwable in some way, and wishes to quit/stop execution */ const QUIT = 0x30; /** * @var RunInterface */ private $run; /** * @var InspectorInterface $inspector */ private $inspector; /** * @var \Throwable $exception */ private $exception; /** * @param RunInterface $run */ public function setRun(RunInterface $run) { $this->run = $run; } /** * @return RunInterface */ protected function getRun() { return $this->run; } /** * @param InspectorInterface $inspector */ public function setInspector(InspectorInterface $inspector) { $this->inspector = $inspector; } /** * @return InspectorInterface */ protected function getInspector() { return $this->inspector; } /** * @param \Throwable $exception */ public function setException($exception) { $this->exception = $exception; } /** * @return \Throwable */ protected function getException() { return $this->exception; } } ��������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/XmlResponseHandler.php���������������������������������������������������������������0000644�����������������00000005243�15025130165�0013670 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use SimpleXMLElement; use Whoops\Exception\Formatter; /** * Catches an exception and converts it to an XML * response. Additionally can also return exception * frames for consumption by an API. */ class XmlResponseHandler extends Handler { /** * @var bool */ private $returnFrames = false; /** * @param bool|null $returnFrames * @return bool|static */ public function addTraceToOutput($returnFrames = null) { if (func_num_args() == 0) { return $this->returnFrames; } $this->returnFrames = (bool) $returnFrames; return $this; } /** * @return int */ public function handle() { $response = [ 'error' => Formatter::formatExceptionAsDataArray( $this->getInspector(), $this->addTraceToOutput(), $this->getRun()->getFrameFilters() ), ]; echo self::toXml($response); return Handler::QUIT; } /** * @return string */ public function contentType() { return 'application/xml'; } /** * @param SimpleXMLElement $node Node to append data to, will be modified in place * @param array|\Traversable $data * @return SimpleXMLElement The modified node, for chaining */ private static function addDataToNode(\SimpleXMLElement $node, $data) { assert(is_array($data) || $data instanceof Traversable); foreach ($data as $key => $value) { if (is_numeric($key)) { // Convert the key to a valid string $key = "unknownNode_". (string) $key; } // Delete any char not allowed in XML element names $key = preg_replace('/[^a-z0-9\-\_\.\:]/i', '', $key); if (is_array($value)) { $child = $node->addChild($key); self::addDataToNode($child, $value); } else { $value = str_replace('&', '&', print_r($value, true)); $node->addChild($key, $value); } } return $node; } /** * The main function for converting to an XML document. * * @param array|\Traversable $data * @return string XML */ private static function toXml($data) { assert(is_array($data) || $data instanceof Traversable); $node = simplexml_load_string("<?xml version='1.0' encoding='utf-8'?><root />"); return self::addDataToNode($node, $data)->asXML(); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Handler/PrettyPageHandler.php����������������������������������������������������������������0000644�����������������00000062244�15025130165�0013501 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops\Handler; use InvalidArgumentException; use RuntimeException; use Symfony\Component\VarDumper\Cloner\AbstractCloner; use Symfony\Component\VarDumper\Cloner\VarCloner; use UnexpectedValueException; use Whoops\Exception\Formatter; use Whoops\Util\Misc; use Whoops\Util\TemplateHelper; class PrettyPageHandler extends Handler { const EDITOR_SUBLIME = "sublime"; const EDITOR_TEXTMATE = "textmate"; const EDITOR_EMACS = "emacs"; const EDITOR_MACVIM = "macvim"; const EDITOR_PHPSTORM = "phpstorm"; const EDITOR_IDEA = "idea"; const EDITOR_VSCODE = "vscode"; const EDITOR_ATOM = "atom"; const EDITOR_ESPRESSO = "espresso"; const EDITOR_XDEBUG = "xdebug"; const EDITOR_NETBEANS = "netbeans"; /** * Search paths to be scanned for resources. * * Stored in the reverse order they're declared. * * @var array */ private $searchPaths = []; /** * Fast lookup cache for known resource locations. * * @var array */ private $resourceCache = []; /** * The name of the custom css file. * * @var string|null */ private $customCss = null; /** * The name of the custom js file. * * @var string|null */ private $customJs = null; /** * @var array[] */ private $extraTables = []; /** * @var bool */ private $handleUnconditionally = false; /** * @var string */ private $pageTitle = "Whoops! There was an error."; /** * @var array[] */ private $applicationPaths; /** * @var array[] */ private $blacklist = [ '_GET' => [], '_POST' => [], '_FILES' => [], '_COOKIE' => [], '_SESSION' => [], '_SERVER' => [], '_ENV' => [], ]; /** * An identifier for a known IDE/text editor. * * Either a string, or a calalble that resolves a string, that can be used * to open a given file in an editor. If the string contains the special * substrings %file or %line, they will be replaced with the correct data. * * @example * "txmt://open?url=%file&line=%line" * * @var callable|string $editor */ protected $editor; /** * A list of known editor strings. * * @var array */ protected $editors = [ "sublime" => "subl://open?url=file://%file&line=%line", "textmate" => "txmt://open?url=file://%file&line=%line", "emacs" => "emacs://open?url=file://%file&line=%line", "macvim" => "mvim://open/?url=file://%file&line=%line", "phpstorm" => "phpstorm://open?file=%file&line=%line", "idea" => "idea://open?file=%file&line=%line", "vscode" => "vscode://file/%file:%line", "atom" => "atom://core/open/file?filename=%file&line=%line", "espresso" => "x-espresso://open?filepath=%file&lines=%line", "netbeans" => "netbeans://open/?f=%file:%line", ]; /** * @var TemplateHelper */ protected $templateHelper; /** * Constructor. * * @return void */ public function __construct() { if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) { // Register editor using xdebug's file_link_format option. $this->editors['xdebug'] = function ($file, $line) { return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')); }; // If xdebug is available, use it as default editor. $this->setEditor('xdebug'); } // Add the default, local resource search path: $this->searchPaths[] = __DIR__ . "/../Resources"; // blacklist php provided auth based values $this->blacklist('_SERVER', 'PHP_AUTH_PW'); $this->templateHelper = new TemplateHelper(); if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { $cloner = new VarCloner(); // Only dump object internals if a custom caster exists for performance reasons // https://github.com/filp/whoops/pull/404 $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) { $class = $stub->class; $classes = [$class => $class] + class_parents($obj) + class_implements($obj); foreach ($classes as $class) { if (isset(AbstractCloner::$defaultCasters[$class])) { return $a; } } // Remove all internals return []; }]); $this->templateHelper->setCloner($cloner); } } /** * @return int|null * * @throws \Exception */ public function handle() { if (!$this->handleUnconditionally()) { // Check conditions for outputting HTML: // @todo: Make this more robust if (PHP_SAPI === 'cli') { // Help users who have been relying on an internal test value // fix their code to the proper method if (isset($_ENV['whoops-test'])) { throw new \Exception( 'Use handleUnconditionally instead of whoops-test' .' environment variable' ); } return Handler::DONE; } } $templateFile = $this->getResource("views/layout.html.php"); $cssFile = $this->getResource("css/whoops.base.css"); $zeptoFile = $this->getResource("js/zepto.min.js"); $prismJs = $this->getResource("js/prism.js"); $prismCss = $this->getResource("css/prism.css"); $clipboard = $this->getResource("js/clipboard.min.js"); $jsFile = $this->getResource("js/whoops.base.js"); if ($this->customCss) { $customCssFile = $this->getResource($this->customCss); } if ($this->customJs) { $customJsFile = $this->getResource($this->customJs); } $inspector = $this->getInspector(); $frames = $this->getExceptionFrames(); $code = $this->getExceptionCode(); // List of variables that will be passed to the layout template. $vars = [ "page_title" => $this->getPageTitle(), // @todo: Asset compiler "stylesheet" => file_get_contents($cssFile), "zepto" => file_get_contents($zeptoFile), "prismJs" => file_get_contents($prismJs), "prismCss" => file_get_contents($prismCss), "clipboard" => file_get_contents($clipboard), "javascript" => file_get_contents($jsFile), // Template paths: "header" => $this->getResource("views/header.html.php"), "header_outer" => $this->getResource("views/header_outer.html.php"), "frame_list" => $this->getResource("views/frame_list.html.php"), "frames_description" => $this->getResource("views/frames_description.html.php"), "frames_container" => $this->getResource("views/frames_container.html.php"), "panel_details" => $this->getResource("views/panel_details.html.php"), "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), "panel_left" => $this->getResource("views/panel_left.html.php"), "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), "frame_code" => $this->getResource("views/frame_code.html.php"), "env_details" => $this->getResource("views/env_details.html.php"), "title" => $this->getPageTitle(), "name" => explode("\\", $inspector->getExceptionName()), "message" => $inspector->getExceptionMessage(), "previousMessages" => $inspector->getPreviousExceptionMessages(), "docref_url" => $inspector->getExceptionDocrefUrl(), "code" => $code, "previousCodes" => $inspector->getPreviousExceptionCodes(), "plain_exception" => Formatter::formatExceptionPlain($inspector), "frames" => $frames, "has_frames" => !!count($frames), "handler" => $this, "handlers" => $this->getRun()->getHandlers(), "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', "has_frames_tabs" => $this->getApplicationPaths(), "tables" => [ "GET Data" => $this->masked($_GET, '_GET'), "POST Data" => $this->masked($_POST, '_POST'), "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], "Cookies" => $this->masked($_COOKIE, '_COOKIE'), "Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [], "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), "Environment Variables" => $this->masked($_ENV, '_ENV'), ], ]; if (isset($customCssFile)) { $vars["stylesheet"] .= file_get_contents($customCssFile); } if (isset($customJsFile)) { $vars["javascript"] .= file_get_contents($customJsFile); } // Add extra entries list of data tables: // @todo: Consolidate addDataTable and addDataTableCallback $extraTables = array_map(function ($table) use ($inspector) { return $table instanceof \Closure ? $table($inspector) : $table; }, $this->getDataTables()); $vars["tables"] = array_merge($extraTables, $vars["tables"]); $plainTextHandler = new PlainTextHandler(); $plainTextHandler->setRun($this->getRun()); $plainTextHandler->setException($this->getException()); $plainTextHandler->setInspector($this->getInspector()); $vars["preface"] = "<!--\n\n\n" . $this->templateHelper->escape($plainTextHandler->generateResponse()) . "\n\n\n\n\n\n\n\n\n\n\n-->"; $this->templateHelper->setVariables($vars); $this->templateHelper->render($templateFile); return Handler::QUIT; } /** * Get the stack trace frames of the exception currently being handled. * * @return \Whoops\Exception\FrameCollection */ protected function getExceptionFrames() { $frames = $this->getInspector()->getFrames($this->getRun()->getFrameFilters()); if ($this->getApplicationPaths()) { foreach ($frames as $frame) { foreach ($this->getApplicationPaths() as $path) { if (strpos($frame->getFile(), $path) === 0) { $frame->setApplication(true); break; } } } } return $frames; } /** * Get the code of the exception currently being handled. * * @return string */ protected function getExceptionCode() { $exception = $this->getException(); $code = $exception->getCode(); if ($exception instanceof \ErrorException) { // ErrorExceptions wrap the php-error types within the 'severity' property $code = Misc::translateErrorCode($exception->getSeverity()); } return (string) $code; } /** * @return string */ public function contentType() { return 'text/html'; } /** * Adds an entry to the list of tables displayed in the template. * * The expected data is a simple associative array. Any nested arrays * will be flattened with `print_r`. * * @param string $label * * @return static */ public function addDataTable($label, array $data) { $this->extraTables[$label] = $data; return $this; } /** * Lazily adds an entry to the list of tables displayed in the table. * * The supplied callback argument will be called when the error is * rendered, it should produce a simple associative array. Any nested * arrays will be flattened with `print_r`. * * @param string $label * @param callable $callback Callable returning an associative array * * @throws InvalidArgumentException If $callback is not callable * * @return static */ public function addDataTableCallback($label, /* callable */ $callback) { if (!is_callable($callback)) { throw new InvalidArgumentException('Expecting callback argument to be callable'); } $this->extraTables[$label] = function (\Whoops\Inspector\InspectorInterface $inspector = null) use ($callback) { try { $result = call_user_func($callback, $inspector); // Only return the result if it can be iterated over by foreach(). return is_array($result) || $result instanceof \Traversable ? $result : []; } catch (\Exception $e) { // Don't allow failure to break the rendering of the original exception. return []; } }; return $this; } /** * Returns all the extra data tables registered with this handler. * * Optionally accepts a 'label' parameter, to only return the data table * under that label. * * @param string|null $label * * @return array[]|callable */ public function getDataTables($label = null) { if ($label !== null) { return isset($this->extraTables[$label]) ? $this->extraTables[$label] : []; } return $this->extraTables; } /** * Set whether to handle unconditionally. * * Allows to disable all attempts to dynamically decide whether to handle * or return prematurely. Set this to ensure that the handler will perform, * no matter what. * * @param bool|null $value * * @return bool|static */ public function handleUnconditionally($value = null) { if (func_num_args() == 0) { return $this->handleUnconditionally; } $this->handleUnconditionally = (bool) $value; return $this; } /** * Adds an editor resolver. * * Either a string, or a closure that resolves a string, that can be used * to open a given file in an editor. If the string contains the special * substrings %file or %line, they will be replaced with the correct data. * * @example * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line") * @example * $run->addEditor('remove-it', function($file, $line) { * unlink($file); * return "http://stackoverflow.com"; * }); * * @param string $identifier * @param string|callable $resolver * * @return static */ public function addEditor($identifier, $resolver) { $this->editors[$identifier] = $resolver; return $this; } /** * Set the editor to use to open referenced files. * * Pass either the name of a configured editor, or a closure that directly * resolves an editor string. * * @example * $run->setEditor(function($file, $line) { return "file:///{$file}"; }); * @example * $run->setEditor('sublime'); * * @param string|callable $editor * * @throws InvalidArgumentException If invalid argument identifier provided * * @return static */ public function setEditor($editor) { if (!is_callable($editor) && !isset($this->editors[$editor])) { throw new InvalidArgumentException( "Unknown editor identifier: $editor. Known editors:" . implode(",", array_keys($this->editors)) ); } $this->editor = $editor; return $this; } /** * Get the editor href for a given file and line, if available. * * @param string $filePath * @param int $line * * @throws InvalidArgumentException If editor resolver does not return a string * * @return string|bool */ public function getEditorHref($filePath, $line) { $editor = $this->getEditor($filePath, $line); if (empty($editor)) { return false; } // Check that the editor is a string, and replace the // %line and %file placeholders: if (!isset($editor['url']) || !is_string($editor['url'])) { throw new UnexpectedValueException( __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead." ); } $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']); $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']); return $editor['url']; } /** * Determine if the editor link should act as an Ajax request. * * @param string $filePath * @param int $line * * @throws UnexpectedValueException If editor resolver does not return a boolean * * @return bool */ public function getEditorAjax($filePath, $line) { $editor = $this->getEditor($filePath, $line); // Check that the ajax is a bool if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) { throw new UnexpectedValueException( __METHOD__ . " should always resolve to a bool; got something else instead." ); } return $editor['ajax']; } /** * Determines both the editor and if ajax should be used. * * @param string $filePath * @param int $line * * @return array */ protected function getEditor($filePath, $line) { if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) { return []; } if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) { return [ 'ajax' => false, 'url' => $this->editors[$this->editor], ]; } if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) { if (is_callable($this->editor)) { $callback = call_user_func($this->editor, $filePath, $line); } else { $callback = call_user_func($this->editors[$this->editor], $filePath, $line); } if (empty($callback)) { return []; } if (is_string($callback)) { return [ 'ajax' => false, 'url' => $callback, ]; } return [ 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false, 'url' => isset($callback['url']) ? $callback['url'] : $callback, ]; } return []; } /** * Set the page title. * * @param string $title * * @return static */ public function setPageTitle($title) { $this->pageTitle = (string) $title; return $this; } /** * Get the page title. * * @return string */ public function getPageTitle() { return $this->pageTitle; } /** * Adds a path to the list of paths to be searched for resources. * * @param string $path * * @throws InvalidArgumentException If $path is not a valid directory * * @return static */ public function addResourcePath($path) { if (!is_dir($path)) { throw new InvalidArgumentException( "'$path' is not a valid directory" ); } array_unshift($this->searchPaths, $path); return $this; } /** * Adds a custom css file to be loaded. * * @param string|null $name * * @return static */ public function addCustomCss($name) { $this->customCss = $name; return $this; } /** * Adds a custom js file to be loaded. * * @param string|null $name * * @return static */ public function addCustomJs($name) { $this->customJs = $name; return $this; } /** * @return array */ public function getResourcePaths() { return $this->searchPaths; } /** * Finds a resource, by its relative path, in all available search paths. * * The search is performed starting at the last search path, and all the * way back to the first, enabling a cascading-type system of overrides for * all resources. * * @param string $resource * * @throws RuntimeException If resource cannot be found in any of the available paths * * @return string */ protected function getResource($resource) { // If the resource was found before, we can speed things up // by caching its absolute, resolved path: if (isset($this->resourceCache[$resource])) { return $this->resourceCache[$resource]; } // Search through available search paths, until we find the // resource we're after: foreach ($this->searchPaths as $path) { $fullPath = $path . "/$resource"; if (is_file($fullPath)) { // Cache the result: $this->resourceCache[$resource] = $fullPath; return $fullPath; } } // If we got this far, nothing was found. throw new RuntimeException( "Could not find resource '$resource' in any resource paths." . "(searched: " . join(", ", $this->searchPaths). ")" ); } /** * @deprecated * * @return string */ public function getResourcesPath() { $allPaths = $this->getResourcePaths(); // Compat: return only the first path added return end($allPaths) ?: null; } /** * @deprecated * * @param string $resourcesPath * * @return static */ public function setResourcesPath($resourcesPath) { $this->addResourcePath($resourcesPath); return $this; } /** * Return the application paths. * * @return array */ public function getApplicationPaths() { return $this->applicationPaths; } /** * Set the application paths. * * @return void */ public function setApplicationPaths(array $applicationPaths) { $this->applicationPaths = $applicationPaths; } /** * Set the application root path. * * @param string $applicationRootPath * * @return void */ public function setApplicationRootPath($applicationRootPath) { $this->templateHelper->setApplicationRootPath($applicationRootPath); } /** * blacklist a sensitive value within one of the superglobal arrays. * Alias for the hideSuperglobalKey method. * * @param string $superGlobalName The name of the superglobal array, e.g. '_GET' * @param string $key The key within the superglobal * @see hideSuperglobalKey * * @return static */ public function blacklist($superGlobalName, $key) { $this->blacklist[$superGlobalName][] = $key; return $this; } /** * Hide a sensitive value within one of the superglobal arrays. * * @param string $superGlobalName The name of the superglobal array, e.g. '_GET' * @param string $key The key within the superglobal * @return static */ public function hideSuperglobalKey($superGlobalName, $key) { return $this->blacklist($superGlobalName, $key); } /** * Checks all values within the given superGlobal array. * * Blacklisted values will be replaced by a equal length string containing * only '*' characters for string values. * Non-string values will be replaced with a fixed asterisk count. * We intentionally dont rely on $GLOBALS as it depends on the 'auto_globals_jit' php.ini setting. * * @param array|\ArrayAccess $superGlobal One of the superglobal arrays * @param string $superGlobalName The name of the superglobal array, e.g. '_GET' * * @return array $values without sensitive data */ private function masked($superGlobal, $superGlobalName) { $blacklisted = $this->blacklist[$superGlobalName]; $values = $superGlobal; foreach ($blacklisted as $key) { if (isset($superGlobal[$key])) { $values[$key] = str_repeat('*', is_string($superGlobal[$key]) ? strlen($superGlobal[$key]) : 3); } } return $values; } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/RunInterface.php�����������������������������������������������������������������������������0000644�����������������00000007702�15025130165�0011125 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Whoops - php errors for cool kids * @author Filipe Dobreira <http://github.com/filp> */ namespace Whoops; use InvalidArgumentException; use Whoops\Exception\ErrorException; use Whoops\Handler\HandlerInterface; interface RunInterface { const EXCEPTION_HANDLER = "handleException"; const ERROR_HANDLER = "handleError"; const SHUTDOWN_HANDLER = "handleShutdown"; /** * Pushes a handler to the end of the stack * * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface * @param Callable|HandlerInterface $handler * @return Run */ public function pushHandler($handler); /** * Removes the last handler in the stack and returns it. * Returns null if there"s nothing else to pop. * * @return null|HandlerInterface */ public function popHandler(); /** * Returns an array with all handlers, in the * order they were added to the stack. * * @return array */ public function getHandlers(); /** * Clears all handlers in the handlerStack, including * the default PrettyPage handler. * * @return Run */ public function clearHandlers(); /** * @return array<callable> */ public function getFrameFilters(); /** * @return Run */ public function clearFrameFilters(); /** * Registers this instance as an error handler. * * @return Run */ public function register(); /** * Unregisters all handlers registered by this Whoops\Run instance * * @return Run */ public function unregister(); /** * Should Whoops allow Handlers to force the script to quit? * * @param bool|int $exit * @return bool */ public function allowQuit($exit = null); /** * Silence particular errors in particular files * * @param array|string $patterns List or a single regex pattern to match * @param int $levels Defaults to E_STRICT | E_DEPRECATED * @return \Whoops\Run */ public function silenceErrorsInPaths($patterns, $levels = 10240); /** * Should Whoops send HTTP error code to the browser if possible? * Whoops will by default send HTTP code 500, but you may wish to * use 502, 503, or another 5xx family code. * * @param bool|int $code * @return int|false */ public function sendHttpCode($code = null); /** * Should Whoops exit with a specific code on the CLI if possible? * Whoops will exit with 1 by default, but you can specify something else. * * @param int $code * @return int */ public function sendExitCode($code = null); /** * Should Whoops push output directly to the client? * If this is false, output will be returned by handleException * * @param bool|int $send * @return bool */ public function writeToOutput($send = null); /** * Handles an exception, ultimately generating a Whoops error * page. * * @param \Throwable $exception * @return string Output generated by handlers */ public function handleException($exception); /** * Converts generic PHP errors to \ErrorException * instances, before passing them off to be handled. * * This method MUST be compatible with set_error_handler. * * @param int $level * @param string $message * @param string $file * @param int $line * * @return bool * @throws ErrorException */ public function handleError($level, $message, $file = null, $line = null); /** * Special case to deal with Fatal errors and the like. */ public function handleShutdown(); /** * Registers a filter callback in the frame filters stack. * * @param callable $filterCallback * @return \Whoops\Run */ public function addFrameFilter($filterCallback); } ��������������������������������������������������������������Whoops/Resources/css/prism.css����������������������������������������������������������������������0000644�����������������00000006212�15025130165�0012430 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+markup-templating+php&plugins=line-highlight+line-numbers */ code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)} pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right} ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Resources/css/whoops.base.css����������������������������������������������������������������0000644�����������������00000024065�15025130165�0013534 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������body { font: 12px "Helvetica Neue", helvetica, arial, sans-serif; color: #131313; background: #eeeeee; padding:0; margin: 0; max-height: 100%; text-rendering: optimizeLegibility; } a { text-decoration: none; } .Whoops.container { position: relative; z-index: 9999999999; } .panel { overflow-y: scroll; height: 100%; position: fixed; margin: 0; left: 0; top: 0; } .branding { position: absolute; top: 10px; right: 20px; color: #777777; font-size: 10px; z-index: 100; } .branding a { color: #e95353; } header { color: white; box-sizing: border-box; background-color: #2a2a2a; padding: 35px 40px; max-height: 180px; overflow: hidden; transition: 0.5s; } header.header-expand { max-height: 1000px; } .exc-title { margin: 0; color: #bebebe; font-size: 14px; } .exc-title-primary, .exc-title-secondary { color: #e95353; } .exc-message { font-size: 20px; word-wrap: break-word; margin: 4px 0 0 0; color: white; } .exc-message span { display: block; } .exc-message-empty-notice { color: #a29d9d; font-weight: 300; } .prev-exc-title { margin: 10px 0; } .prev-exc-title + ul { margin: 0; padding: 0 0 0 20px; line-height: 12px; } .prev-exc-title + ul li { font: 12px "Helvetica Neue", helvetica, arial, sans-serif; } .prev-exc-title + ul li .prev-exc-code { display: inline-block; color: #bebebe; } .details-container { left: 30%; width: 70%; background: #fafafa; } .details { padding: 5px; } .details-heading { color: #4288CE; font-weight: 300; padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid rgba(0, 0, 0, .1); } .details pre.sf-dump { white-space: pre; word-wrap: inherit; } .details pre.sf-dump, .details pre.sf-dump .sf-dump-num, .details pre.sf-dump .sf-dump-const, .details pre.sf-dump .sf-dump-str, .details pre.sf-dump .sf-dump-note, .details pre.sf-dump .sf-dump-ref, .details pre.sf-dump .sf-dump-public, .details pre.sf-dump .sf-dump-protected, .details pre.sf-dump .sf-dump-private, .details pre.sf-dump .sf-dump-meta, .details pre.sf-dump .sf-dump-key, .details pre.sf-dump .sf-dump-index { color: #463C54; } .left-panel { width: 30%; background: #ded8d8; } .frames-description { background: rgba(0, 0, 0, .05); padding: 8px 15px; color: #a29d9d; font-size: 11px; } .frames-description.frames-description-application { text-align: center; font-size: 12px; } .frames-container.frames-container-application .frame:not(.frame-application) { display: none; } .frames-tab { color: #a29d9d; display: inline-block; padding: 4px 8px; margin: 0 2px; border-radius: 3px; } .frames-tab.frames-tab-active { background-color: #2a2a2a; color: #bebebe; } .frame { padding: 14px; cursor: pointer; transition: all 0.1s ease; background: #eeeeee; } .frame:not(:last-child) { border-bottom: 1px solid rgba(0, 0, 0, .05); } .frame.active { box-shadow: inset -5px 0 0 0 #4288CE; color: #4288CE; } .frame:not(.active):hover { background: #BEE9EA; } .frame-method-info { margin-bottom: 10px; } .frame-class, .frame-function, .frame-index { font-size: 14px; } .frame-index { float: left; } .frame-method-info { margin-left: 24px; } .frame-index { font-size: 11px; color: #a29d9d; background-color: rgba(0, 0, 0, .05); height: 18px; width: 18px; line-height: 18px; border-radius: 5px; padding: 0 1px 0 1px; text-align: center; display: inline-block; } .frame-application .frame-index { background-color: #2a2a2a; color: #bebebe; } .frame-file { font-family: "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace; color: #a29d9d; } .frame-file .editor-link { color: #a29d9d; } .frame-line { font-weight: bold; } .frame-line:before { content: ":"; } .frame-code { padding: 5px; background: #303030; display: none; } .frame-code.active { display: block; } .frame-code .frame-file { color: #a29d9d; padding: 12px 6px; border-bottom: none; } .code-block { padding: 10px; margin: 0; border-radius: 6px; box-shadow: 0 3px 0 rgba(0, 0, 0, .05), 0 10px 30px rgba(0, 0, 0, .05), inset 0 0 1px 0 rgba(255, 255, 255, .07); -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; } .linenums { margin: 0; margin-left: 10px; } .frame-comments { border-top: none; margin-top: 15px; font-size: 12px; } .frame-comments.empty { } .frame-comments.empty:before { content: "No comments for this stack frame."; font-weight: 300; color: #a29d9d; } .frame-comment { padding: 10px; color: #e3e3e3; border-radius: 6px; background-color: rgba(255, 255, 255, .05); } .frame-comment a { font-weight: bold; text-decoration: none; } .frame-comment a:hover { color: #4bb1b1; } .frame-comment:not(:last-child) { border-bottom: 1px dotted rgba(0, 0, 0, .3); } .frame-comment-context { font-size: 10px; color: white; } .delimiter { display: inline-block; } .data-table-container label { font-size: 16px; color: #303030; font-weight: bold; margin: 10px 0; display: block; margin-bottom: 5px; padding-bottom: 5px; } .data-table { width: 100%; margin-bottom: 10px; } .data-table tbody { font: 13px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace; } .data-table thead { display: none; } .data-table tr { padding: 5px 0; } .data-table td:first-child { width: 20%; min-width: 130px; overflow: hidden; font-weight: bold; color: #463C54; padding-right: 5px; } .data-table td:last-child { width: 80%; -ms-word-break: break-all; word-break: break-all; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; } .data-table span.empty { color: rgba(0, 0, 0, .3); font-weight: 300; } .data-table label.empty { display: inline; } .handler { padding: 4px 0; font: 14px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace; } #plain-exception { display: none; } .rightButton { cursor: pointer; border: 0; opacity: .8; background: none; color: rgba(255, 255, 255, 0.1); box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1); border-radius: 3px; outline: none !important; } .rightButton:hover { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.3); } /* inspired by githubs kbd styles */ kbd { -moz-border-bottom-colors: none; -moz-border-left-colors: none; -moz-border-right-colors: none; -moz-border-top-colors: none; background-color: #fcfcfc; border-color: #ccc #ccc #bbb; border-image: none; border-style: solid; border-width: 1px; color: #555; display: inline-block; font-size: 11px; line-height: 10px; padding: 3px 5px; vertical-align: middle; } /* == Media queries */ /* Expand the spacing in the details section */ @media (min-width: 1000px) { .details, .frame-code { padding: 20px 40px; } .details-container { left: 32%; width: 68%; } .frames-container { margin: 5px; } .left-panel { width: 32%; } } /* Stack panels */ @media (max-width: 600px) { .panel { position: static; width: 100%; } } /* Stack details tables */ @media (max-width: 400px) { .data-table, .data-table tbody, .data-table tbody tr, .data-table tbody td { display: block; width: 100%; } .data-table tbody tr:first-child { padding-top: 0; } .data-table tbody td:first-child, .data-table tbody td:last-child { padding-left: 0; padding-right: 0; } .data-table tbody td:last-child { padding-top: 3px; } } .tooltipped { position: relative } .tooltipped:after { position: absolute; z-index: 1000000; display: none; padding: 5px 8px; color: #fff; text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-wrap: break-word; white-space: pre; pointer-events: none; content: attr(aria-label); background: rgba(0, 0, 0, 0.8); border-radius: 3px; -webkit-font-smoothing: subpixel-antialiased } .tooltipped:before { position: absolute; z-index: 1000001; display: none; width: 0; height: 0; color: rgba(0, 0, 0, 0.8); pointer-events: none; content: ""; border: 5px solid transparent } .tooltipped:hover:before, .tooltipped:hover:after, .tooltipped:active:before, .tooltipped:active:after, .tooltipped:focus:before, .tooltipped:focus:after { display: inline-block; text-decoration: none } .tooltipped-s:after { top: 100%; right: 50%; margin-top: 5px } .tooltipped-s:before { top: auto; right: 50%; bottom: -5px; margin-right: -5px; border-bottom-color: rgba(0, 0, 0, 0.8) } pre.sf-dump { padding: 0px !important; margin: 0px !important; } .search-for-help { width: 85%; padding: 0; margin: 10px 0; list-style-type: none; display: inline-block; } .search-for-help li { display: inline-block; margin-right: 5px; } .search-for-help li:last-child { margin-right: 0; } .search-for-help li a { } .search-for-help li a i { width: 16px; height: 16px; overflow: hidden; display: block; } .search-for-help li a svg { fill: #fff; } .search-for-help li a svg path { background-size: contain; } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Resources/views/frames_description.html.php��������������������������������������������������0000644�����������������00000001224�15025130165�0016463 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div class="frames-description <?php echo $has_frames_tabs ? 'frames-description-application' : '' ?>"> <?php if ($has_frames_tabs): ?> <a href="#" id="application-frames-tab" class="frames-tab <?php echo $active_frames_tab == 'application' ? 'frames-tab-active' : '' ?>"> Application frames (<?php echo $frames->countIsApplication() ?>) </a> <a href="#" id="all-frames-tab" class="frames-tab <?php echo $active_frames_tab == 'all' ? 'frames-tab-active' : '' ?>"> All frames (<?php echo count($frames) ?>) </a> <?php else: ?> <span> Stack frames (<?php echo count($frames) ?>) </span> <?php endif; ?> </div> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Whoops/Resources/views/layout.html.php��������������������������������������������������������������0000644�����������������00000001555�15025130165�0014127 0����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?php /** * Layout template file for Whoops's pretty error output. */ ?> <!DOCTYPE html><?php echo $preface; ?> <html> <head> <meta charset="utf-8"> <meta name="robots" content="noindex,nofollow"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/> <title><?php echo $tpl->escape($page_title) ?>
render($panel_left_outer) ?> render($panel_details_outer) ?>
Whoops/Resources/views/panel_details.html.php000064400000000106150251301650015405 0ustar00render($frame_code) ?> render($env_details) ?>Whoops/Resources/views/env_details.html.php000064400000003031150251301650015076 0ustar00

Environment & details:

$data): ?>
$value): ?>
Key Value
escape($k) ?> dump($value) ?>
empty
$h): ?>
. escape(get_class($h)) ?>
Whoops/Resources/views/panel_left_outer.html.php000064400000000171150251301650016132 0ustar00
render($panel_left) ?>
Whoops/Resources/views/panel_details_outer.html.php000064400000000127150251301650016626 0ustar00
render($panel_details) ?>
Whoops/Resources/views/frame_code.html.php000064400000005471150251301650014677 0ustar00
$frame): ?> getLine(); ?>
getFile(); ?> getEditorHref($filePath, (int) $line)): ?> getEditorAjax($filePath, (int) $line) ? ' data-ajax' : '') ?>> Open: breakOnDelimiter('/', $tpl->escape($filePath ?: '<#unknown>')) ?> breakOnDelimiter('/', $tpl->escape($filePath ?: '<#unknown>')) ?>
getFileLines($line - 20, 40); // getFileLines can return null if there is no source code if ($range): $range = array_map(function ($line) { return empty($line) ? ' ' : $line;}, $range); $start = key($range) + 1; $code = join("\n", $range); ?>
escape($code) ?>
dumpArgs($frame); ?>
Arguments
getComments(); ?>
$comment): ?>
escape($context) ?> escapeButPreserveUris($comment) ?>
Whoops/Resources/views/frame_list.html.php000064400000002017150251301650014731 0ustar00 $frame): ?>
breakOnDelimiter('\\', $tpl->escape($frame->getClass() ?: '')) ?> breakOnDelimiter('\\', $tpl->escape($frame->getFunction() ?: '')) ?>
getFile() ? $tpl->breakOnDelimiter('/', $tpl->shorten($tpl->escape($frame->getFile()))) : '<#unknown>' ?>getLine() ?>
render($header_outer); $tpl->render($frames_description); $tpl->render($frames_container); Whoops/Resources/views/header_outer.html.php000064400000000064150251301650015252 0ustar00
render($header) ?>
Whoops/Resources/views/frames_container.html.php000064400000000242150251301650016121 0ustar00
render($frame_list) ?>
Whoops/Resources/views/header.html.php000064400000022311150251301650014033 0ustar00
$nameSection): ?> escape($nameSection) ?> escape($nameSection) . ' \\' ?> (escape($code) ?>)
escape($message) ?>
Previous exceptions
    $previousMessage): ?>
  • escape($previousMessage) ?> ()
No message escape($plain_exception) ?>
Whoops/Resources/js/prism.js000064400000057076150251301650012116 0ustar00/* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+markup-templating+php&plugins=line-highlight+line-numbers */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; !function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism); !function(e){var a=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,t=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],i=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,n=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,s=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:a,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:t,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:i,operator:n,punctuation:s};var l={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},r=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:l}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:l}}];e.languages.insertBefore("php","variable",{string:r,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:a,string:r,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:t,number:i,operator:n,punctuation:s}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(a){/<\?/.test(a.code)&&e.languages["markup-templating"].buildPlaceholders(a,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g)})),e.hooks.add("after-tokenize",(function(a){e.languages["markup-templating"].tokenizePlaceholders(a,"php")}))}(Prism); !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document&&document.querySelector){var e,t="line-numbers",i="linkable-line-numbers",n=/\n(?!$)/g,r=!0;Prism.plugins.lineHighlight={highlightLines:function(o,u,c){var h=(u="string"==typeof u?u:o.getAttribute("data-line")||"").replace(/\s+/g,"").split(",").filter(Boolean),d=+o.getAttribute("data-line-offset")||0,f=(function(){if(void 0===e){var t=document.createElement("div");t.style.fontSize="13px",t.style.lineHeight="1.5",t.style.padding="0",t.style.border="0",t.innerHTML=" 
 ",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}()?parseInt:parseFloat)(getComputedStyle(o).lineHeight),p=Prism.util.isActive(o,t),g=o.querySelector("code"),m=p?o:g||o,v=[],y=g.textContent.match(n),b=y?y.length+1:1,A=g&&m!=g?function(e,t){var i=getComputedStyle(e),n=getComputedStyle(t);function r(e){return+e.substr(0,e.length-2)}return t.offsetTop+r(n.borderTopWidth)+r(n.paddingTop)-r(i.paddingTop)}(o,g):0;h.forEach((function(e){var t=e.split("-"),i=+t[0],n=+t[1]||i;if(!((n=Math.min(b+d,n))i&&r.setAttribute("data-end",String(n)),r.style.top=(i-d-1)*f+A+"px",r.textContent=new Array(n-i+2).join(" \n")}));v.push((function(){r.style.width=o.scrollWidth+"px"})),v.push((function(){m.appendChild(r)}))}}));var P=o.id;if(p&&Prism.util.isActive(o,i)&&P){l(o,i)||v.push((function(){o.classList.add(i)}));var E=parseInt(o.getAttribute("data-start")||"1");s(".line-numbers-rows > span",o).forEach((function(e,t){var i=t+E;e.onclick=function(){var e=P+"."+i;r=!1,location.hash=e,setTimeout((function(){r=!0}),1)}}))}return function(){v.forEach(a)}}};var o=0;Prism.hooks.add("before-sanity-check",(function(e){var t=e.element.parentElement;if(u(t)){var i=0;s(".line-highlight",t).forEach((function(e){i+=e.textContent.length,e.parentNode.removeChild(e)})),i&&/^(?: \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}})),Prism.hooks.add("complete",(function e(i){var n=i.element.parentElement;if(u(n)){clearTimeout(o);var r=Prism.plugins.lineNumbers,s=i.plugins&&i.plugins.lineNumbers;l(n,t)&&r&&!s?Prism.hooks.add("line-numbers",e):(Prism.plugins.lineHighlight.highlightLines(n)(),o=setTimeout(c,1))}})),window.addEventListener("hashchange",c),window.addEventListener("resize",(function(){s("pre").filter(u).map((function(e){return Prism.plugins.lineHighlight.highlightLines(e)})).forEach(a)}))}function s(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function l(e,t){return e.classList.contains(t)}function a(e){e()}function u(e){return!!(e&&/pre/i.test(e.nodeName)&&(e.hasAttribute("data-line")||e.id&&Prism.util.isActive(e,i)))}function c(){var e=location.hash.slice(1);s(".temporary.line-highlight").forEach((function(e){e.parentNode.removeChild(e)}));var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var i=e.slice(0,e.lastIndexOf(".")),n=document.getElementById(i);n&&(n.hasAttribute("data-line")||n.setAttribute("data-line",""),Prism.plugins.lineHighlight.highlightLines(n,t,"temporary ")(),r&&document.querySelector(".temporary.line-highlight").scrollIntoView())}}}(); !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);ts&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;r0?n.fn.concat.apply([],t):t}function F(t){return t.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function q(t){return t in f?f[t]:f[t]=new RegExp("(^|\\s)"+t+"(\\s|$)")}function H(t,e){return"number"!=typeof e||c[F(t)]?e:e+"px"}function I(t){var e,n;return u[t]||(e=a.createElement(t),a.body.appendChild(e),n=getComputedStyle(e,"").getPropertyValue("display"),e.parentNode.removeChild(e),"none"==n&&(n="block"),u[t]=n),u[t]}function V(t){return"children"in t?o.call(t.children):n.map(t.childNodes,function(t){return 1==t.nodeType?t:void 0})}function U(n,i,r){for(e in i)r&&(R(i[e])||A(i[e]))?(R(i[e])&&!R(n[e])&&(n[e]={}),A(i[e])&&!A(n[e])&&(n[e]=[]),U(n[e],i[e],r)):i[e]!==t&&(n[e]=i[e])}function B(t,e){return null==e?n(t):n(t).filter(e)}function J(t,e,n,i){return Z(e)?e.call(t,n,i):e}function X(t,e,n){null==n?t.removeAttribute(e):t.setAttribute(e,n)}function W(e,n){var i=e.className,r=i&&i.baseVal!==t;return n===t?r?i.baseVal:i:void(r?i.baseVal=n:e.className=n)}function Y(t){var e;try{return t?"true"==t||("false"==t?!1:"null"==t?null:/^0/.test(t)||isNaN(e=Number(t))?/^[\[\{]/.test(t)?n.parseJSON(t):t:e):t}catch(i){return t}}function G(t,e){e(t);for(var n in t.childNodes)G(t.childNodes[n],e)}var t,e,n,i,C,N,r=[],o=r.slice,s=r.filter,a=window.document,u={},f={},c={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},l=/^\s*<(\w+|!)[^>]*>/,h=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,p=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,d=/^(?:body|html)$/i,m=/([A-Z])/g,g=["val","css","html","text","data","width","height","offset"],v=["after","prepend","before","append"],y=a.createElement("table"),x=a.createElement("tr"),b={tr:a.createElement("tbody"),tbody:y,thead:y,tfoot:y,td:x,th:x,"*":a.createElement("div")},w=/complete|loaded|interactive/,E=/^[\w-]*$/,j={},T=j.toString,S={},O=a.createElement("div"),P={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},A=Array.isArray||function(t){return t instanceof Array};return S.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var n=t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(n)return n.call(t,e);var i,r=t.parentNode,o=!r;return o&&(r=O).appendChild(t),i=~S.qsa(r,e).indexOf(t),o&&O.removeChild(t),i},C=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},N=function(t){return s.call(t,function(e,n){return t.indexOf(e)==n})},S.fragment=function(e,i,r){var s,u,f;return h.test(e)&&(s=n(a.createElement(RegExp.$1))),s||(e.replace&&(e=e.replace(p,"<$1>")),i===t&&(i=l.test(e)&&RegExp.$1),i in b||(i="*"),f=b[i],f.innerHTML=""+e,s=n.each(o.call(f.childNodes),function(){f.removeChild(this)})),R(r)&&(u=n(s),n.each(r,function(t,e){g.indexOf(t)>-1?u[t](e):u.attr(t,e)})),s},S.Z=function(t,e){return t=t||[],t.__proto__=n.fn,t.selector=e||"",t},S.isZ=function(t){return t instanceof S.Z},S.init=function(e,i){var r;if(!e)return S.Z();if("string"==typeof e)if(e=e.trim(),"<"==e[0]&&l.test(e))r=S.fragment(e,RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}else{if(Z(e))return n(a).ready(e);if(S.isZ(e))return e;if(A(e))r=k(e);else if(D(e))r=[e],e=null;else if(l.test(e))r=S.fragment(e.trim(),RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}}return S.Z(r,e)},n=function(t,e){return S.init(t,e)},n.extend=function(t){var e,n=o.call(arguments,1);return"boolean"==typeof t&&(e=t,t=n.shift()),n.forEach(function(n){U(t,n,e)}),t},S.qsa=function(t,e){var n,i="#"==e[0],r=!i&&"."==e[0],s=i||r?e.slice(1):e,a=E.test(s);return _(t)&&a&&i?(n=t.getElementById(s))?[n]:[]:1!==t.nodeType&&9!==t.nodeType?[]:o.call(a&&!i?r?t.getElementsByClassName(s):t.getElementsByTagName(e):t.querySelectorAll(e))},n.contains=function(t,e){return t!==e&&t.contains(e)},n.type=L,n.isFunction=Z,n.isWindow=$,n.isArray=A,n.isPlainObject=R,n.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},n.inArray=function(t,e,n){return r.indexOf.call(e,t,n)},n.camelCase=C,n.trim=function(t){return null==t?"":String.prototype.trim.call(t)},n.uuid=0,n.support={},n.expr={},n.map=function(t,e){var n,r,o,i=[];if(M(t))for(r=0;r=0?e:e+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(t){return r.every.call(this,function(e,n){return t.call(e,n,e)!==!1}),this},filter:function(t){return Z(t)?this.not(this.not(t)):n(s.call(this,function(e){return S.matches(e,t)}))},add:function(t,e){return n(N(this.concat(n(t,e))))},is:function(t){return this.length>0&&S.matches(this[0],t)},not:function(e){var i=[];if(Z(e)&&e.call!==t)this.each(function(t){e.call(this,t)||i.push(this)});else{var r="string"==typeof e?this.filter(e):M(e)&&Z(e.item)?o.call(e):n(e);this.forEach(function(t){r.indexOf(t)<0&&i.push(t)})}return n(i)},has:function(t){return this.filter(function(){return D(t)?n.contains(this,t):n(this).find(t).size()})},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},first:function(){var t=this[0];return t&&!D(t)?t:n(t)},last:function(){var t=this[this.length-1];return t&&!D(t)?t:n(t)},find:function(t){var e,i=this;return e="object"==typeof t?n(t).filter(function(){var t=this;return r.some.call(i,function(e){return n.contains(e,t)})}):1==this.length?n(S.qsa(this[0],t)):this.map(function(){return S.qsa(this,t)})},closest:function(t,e){var i=this[0],r=!1;for("object"==typeof t&&(r=n(t));i&&!(r?r.indexOf(i)>=0:S.matches(i,t));)i=i!==e&&!_(i)&&i.parentNode;return n(i)},parents:function(t){for(var e=[],i=this;i.length>0;)i=n.map(i,function(t){return(t=t.parentNode)&&!_(t)&&e.indexOf(t)<0?(e.push(t),t):void 0});return B(e,t)},parent:function(t){return B(N(this.pluck("parentNode")),t)},children:function(t){return B(this.map(function(){return V(this)}),t)},contents:function(){return this.map(function(){return o.call(this.childNodes)})},siblings:function(t){return B(this.map(function(t,e){return s.call(V(e.parentNode),function(t){return t!==e})}),t)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(t){return n.map(this,function(e){return e[t]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=""),"none"==getComputedStyle(this,"").getPropertyValue("display")&&(this.style.display=I(this.nodeName))})},replaceWith:function(t){return this.before(t).remove()},wrap:function(t){var e=Z(t);if(this[0]&&!e)var i=n(t).get(0),r=i.parentNode||this.length>1;return this.each(function(o){n(this).wrapAll(e?t.call(this,o):r?i.cloneNode(!0):i)})},wrapAll:function(t){if(this[0]){n(this[0]).before(t=n(t));for(var e;(e=t.children()).length;)t=e.first();n(t).append(this)}return this},wrapInner:function(t){var e=Z(t);return this.each(function(i){var r=n(this),o=r.contents(),s=e?t.call(this,i):t;o.length?o.wrapAll(s):r.append(s)})},unwrap:function(){return this.parent().each(function(){n(this).replaceWith(n(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(e){return this.each(function(){var i=n(this);(e===t?"none"==i.css("display"):e)?i.show():i.hide()})},prev:function(t){return n(this.pluck("previousElementSibling")).filter(t||"*")},next:function(t){return n(this.pluck("nextElementSibling")).filter(t||"*")},html:function(t){return 0===arguments.length?this.length>0?this[0].innerHTML:null:this.each(function(e){var i=this.innerHTML;n(this).empty().append(J(this,t,e,i))})},text:function(e){return 0===arguments.length?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=e===t?"":""+e})},attr:function(n,i){var r;return"string"==typeof n&&i===t?0==this.length||1!==this[0].nodeType?t:"value"==n&&"INPUT"==this[0].nodeName?this.val():!(r=this[0].getAttribute(n))&&n in this[0]?this[0][n]:r:this.each(function(t){if(1===this.nodeType)if(D(n))for(e in n)X(this,e,n[e]);else X(this,n,J(this,i,t,this.getAttribute(n)))})},removeAttr:function(t){return this.each(function(){1===this.nodeType&&X(this,t)})},prop:function(e,n){return e=P[e]||e,n===t?this[0]&&this[0][e]:this.each(function(t){this[e]=J(this,n,t,this[e])})},data:function(e,n){var i=this.attr("data-"+e.replace(m,"-$1").toLowerCase(),n);return null!==i?Y(i):t},val:function(t){return 0===arguments.length?this[0]&&(this[0].multiple?n(this[0]).find("option").filter(function(){return this.selected}).pluck("value"):this[0].value):this.each(function(e){this.value=J(this,t,e,this.value)})},offset:function(t){if(t)return this.each(function(e){var i=n(this),r=J(this,t,e,i.offset()),o=i.offsetParent().offset(),s={top:r.top-o.top,left:r.left-o.left};"static"==i.css("position")&&(s.position="relative"),i.css(s)});if(0==this.length)return null;var e=this[0].getBoundingClientRect();return{left:e.left+window.pageXOffset,top:e.top+window.pageYOffset,width:Math.round(e.width),height:Math.round(e.height)}},css:function(t,i){if(arguments.length<2){var r=this[0],o=getComputedStyle(r,"");if(!r)return;if("string"==typeof t)return r.style[C(t)]||o.getPropertyValue(t);if(A(t)){var s={};return n.each(A(t)?t:[t],function(t,e){s[e]=r.style[C(e)]||o.getPropertyValue(e)}),s}}var a="";if("string"==L(t))i||0===i?a=F(t)+":"+H(t,i):this.each(function(){this.style.removeProperty(F(t))});else for(e in t)t[e]||0===t[e]?a+=F(e)+":"+H(e,t[e])+";":this.each(function(){this.style.removeProperty(F(e))});return this.each(function(){this.style.cssText+=";"+a})},index:function(t){return t?this.indexOf(n(t)[0]):this.parent().children().indexOf(this[0])},hasClass:function(t){return t?r.some.call(this,function(t){return this.test(W(t))},q(t)):!1},addClass:function(t){return t?this.each(function(e){i=[];var r=W(this),o=J(this,t,e,r);o.split(/\s+/g).forEach(function(t){n(this).hasClass(t)||i.push(t)},this),i.length&&W(this,r+(r?" ":"")+i.join(" "))}):this},removeClass:function(e){return this.each(function(n){return e===t?W(this,""):(i=W(this),J(this,e,n,i).split(/\s+/g).forEach(function(t){i=i.replace(q(t)," ")}),void W(this,i.trim()))})},toggleClass:function(e,i){return e?this.each(function(r){var o=n(this),s=J(this,e,r,W(this));s.split(/\s+/g).forEach(function(e){(i===t?!o.hasClass(e):i)?o.addClass(e):o.removeClass(e)})}):this},scrollTop:function(e){if(this.length){var n="scrollTop"in this[0];return e===t?n?this[0].scrollTop:this[0].pageYOffset:this.each(n?function(){this.scrollTop=e}:function(){this.scrollTo(this.scrollX,e)})}},scrollLeft:function(e){if(this.length){var n="scrollLeft"in this[0];return e===t?n?this[0].scrollLeft:this[0].pageXOffset:this.each(n?function(){this.scrollLeft=e}:function(){this.scrollTo(e,this.scrollY)})}},position:function(){if(this.length){var t=this[0],e=this.offsetParent(),i=this.offset(),r=d.test(e[0].nodeName)?{top:0,left:0}:e.offset();return i.top-=parseFloat(n(t).css("margin-top"))||0,i.left-=parseFloat(n(t).css("margin-left"))||0,r.top+=parseFloat(n(e[0]).css("border-top-width"))||0,r.left+=parseFloat(n(e[0]).css("border-left-width"))||0,{top:i.top-r.top,left:i.left-r.left}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||a.body;t&&!d.test(t.nodeName)&&"static"==n(t).css("position");)t=t.offsetParent;return t})}},n.fn.detach=n.fn.remove,["width","height"].forEach(function(e){var i=e.replace(/./,function(t){return t[0].toUpperCase()});n.fn[e]=function(r){var o,s=this[0];return r===t?$(s)?s["inner"+i]:_(s)?s.documentElement["scroll"+i]:(o=this.offset())&&o[e]:this.each(function(t){s=n(this),s.css(e,J(this,r,t,s[e]()))})}}),v.forEach(function(t,e){var i=e%2;n.fn[t]=function(){var t,o,r=n.map(arguments,function(e){return t=L(e),"object"==t||"array"==t||null==e?e:S.fragment(e)}),s=this.length>1;return r.length<1?this:this.each(function(t,a){o=i?a:a.parentNode,a=0==e?a.nextSibling:1==e?a.firstChild:2==e?a:null,r.forEach(function(t){if(s)t=t.cloneNode(!0);else if(!o)return n(t).remove();G(o.insertBefore(t,a),function(t){null==t.nodeName||"SCRIPT"!==t.nodeName.toUpperCase()||t.type&&"text/javascript"!==t.type||t.src||window.eval.call(window,t.innerHTML)})})})},n.fn[i?t+"To":"insert"+(e?"Before":"After")]=function(e){return n(e)[t](this),this}}),S.Z.prototype=n.fn,S.uniq=N,S.deserializeValue=Y,n.zepto=S,n}();window.Zepto=Zepto,void 0===window.$&&(window.$=Zepto),function(t){function l(t){return t._zid||(t._zid=e++)}function h(t,e,n,i){if(e=p(e),e.ns)var r=d(e.ns);return(s[l(t)]||[]).filter(function(t){return!(!t||e.e&&t.e!=e.e||e.ns&&!r.test(t.ns)||n&&l(t.fn)!==l(n)||i&&t.sel!=i)})}function p(t){var e=(""+t).split(".");return{e:e[0],ns:e.slice(1).sort().join(" ")}}function d(t){return new RegExp("(?:^| )"+t.replace(" "," .* ?")+"(?: |$)")}function m(t,e){return t.del&&!u&&t.e in f||!!e}function g(t){return c[t]||u&&f[t]||t}function v(e,i,r,o,a,u,f){var h=l(e),d=s[h]||(s[h]=[]);i.split(/\s/).forEach(function(i){if("ready"==i)return t(document).ready(r);var s=p(i);s.fn=r,s.sel=a,s.e in c&&(r=function(e){var n=e.relatedTarget;return!n||n!==this&&!t.contains(this,n)?s.fn.apply(this,arguments):void 0}),s.del=u;var l=u||r;s.proxy=function(t){if(t=j(t),!t.isImmediatePropagationStopped()){t.data=o;var i=l.apply(e,t._args==n?[t]:[t].concat(t._args));return i===!1&&(t.preventDefault(),t.stopPropagation()),i}},s.i=d.length,d.push(s),"addEventListener"in e&&e.addEventListener(g(s.e),s.proxy,m(s,f))})}function y(t,e,n,i,r){var o=l(t);(e||"").split(/\s/).forEach(function(e){h(t,e,n,i).forEach(function(e){delete s[o][e.i],"removeEventListener"in t&&t.removeEventListener(g(e.e),e.proxy,m(e,r))})})}function j(e,i){return(i||!e.isDefaultPrevented)&&(i||(i=e),t.each(E,function(t,n){var r=i[t];e[t]=function(){return this[n]=x,r&&r.apply(i,arguments)},e[n]=b}),(i.defaultPrevented!==n?i.defaultPrevented:"returnValue"in i?i.returnValue===!1:i.getPreventDefault&&i.getPreventDefault())&&(e.isDefaultPrevented=x)),e}function T(t){var e,i={originalEvent:t};for(e in t)w.test(e)||t[e]===n||(i[e]=t[e]);return j(i,t)}var n,e=1,i=Array.prototype.slice,r=t.isFunction,o=function(t){return"string"==typeof t},s={},a={},u="onfocusin"in window,f={focus:"focusin",blur:"focusout"},c={mouseenter:"mouseover",mouseleave:"mouseout"};a.click=a.mousedown=a.mouseup=a.mousemove="MouseEvents",t.event={add:v,remove:y},t.proxy=function(e,n){if(r(e)){var i=function(){return e.apply(n,arguments)};return i._zid=l(e),i}if(o(n))return t.proxy(e[n],e);throw new TypeError("expected function")},t.fn.bind=function(t,e,n){return this.on(t,e,n)},t.fn.unbind=function(t,e){return this.off(t,e)},t.fn.one=function(t,e,n,i){return this.on(t,e,n,i,1)};var x=function(){return!0},b=function(){return!1},w=/^([A-Z]|returnValue$|layer[XY]$)/,E={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};t.fn.delegate=function(t,e,n){return this.on(e,t,n)},t.fn.undelegate=function(t,e,n){return this.off(e,t,n)},t.fn.live=function(e,n){return t(document.body).delegate(this.selector,e,n),this},t.fn.die=function(e,n){return t(document.body).undelegate(this.selector,e,n),this},t.fn.on=function(e,s,a,u,f){var c,l,h=this;return e&&!o(e)?(t.each(e,function(t,e){h.on(t,s,a,e,f)}),h):(o(s)||r(u)||u===!1||(u=a,a=s,s=n),(r(a)||a===!1)&&(u=a,a=n),u===!1&&(u=b),h.each(function(n,r){f&&(c=function(t){return y(r,t.type,u),u.apply(this,arguments)}),s&&(l=function(e){var n,o=t(e.target).closest(s,r).get(0);return o&&o!==r?(n=t.extend(T(e),{currentTarget:o,liveFired:r}),(c||u).apply(o,[n].concat(i.call(arguments,1)))):void 0}),v(r,e,u,a,s,l||c)}))},t.fn.off=function(e,i,s){var a=this;return e&&!o(e)?(t.each(e,function(t,e){a.off(t,i,e)}),a):(o(i)||r(s)||s===!1||(s=i,i=n),s===!1&&(s=b),a.each(function(){y(this,e,s,i)}))},t.fn.trigger=function(e,n){return e=o(e)||t.isPlainObject(e)?t.Event(e):j(e),e._args=n,this.each(function(){"dispatchEvent"in this?this.dispatchEvent(e):t(this).triggerHandler(e,n)})},t.fn.triggerHandler=function(e,n){var i,r;return this.each(function(s,a){i=T(o(e)?t.Event(e):e),i._args=n,i.target=a,t.each(h(a,e.type||e),function(t,e){return r=e.proxy(i),i.isImmediatePropagationStopped()?!1:void 0})}),r},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.trigger(e)}}),["focus","blur"].forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.each(function(){try{this[e]()}catch(t){}}),this}}),t.Event=function(t,e){o(t)||(e=t,t=e.type);var n=document.createEvent(a[t]||"Events"),i=!0;if(e)for(var r in e)"bubbles"==r?i=!!e[r]:n[r]=e[r];return n.initEvent(t,i,!0),j(n)}}(Zepto),function(t){function l(e,n,i){var r=t.Event(n);return t(e).trigger(r,i),!r.isDefaultPrevented()}function h(t,e,i,r){return t.global?l(e||n,i,r):void 0}function p(e){e.global&&0===t.active++&&h(e,null,"ajaxStart")}function d(e){e.global&&!--t.active&&h(e,null,"ajaxStop")}function m(t,e){var n=e.context;return e.beforeSend.call(n,t,e)===!1||h(e,n,"ajaxBeforeSend",[t,e])===!1?!1:void h(e,n,"ajaxSend",[t,e])}function g(t,e,n,i){var r=n.context,o="success";n.success.call(r,t,o,e),i&&i.resolveWith(r,[t,o,e]),h(n,r,"ajaxSuccess",[e,n,t]),y(o,e,n)}function v(t,e,n,i,r){var o=i.context;i.error.call(o,n,e,t),r&&r.rejectWith(o,[n,e,t]),h(i,o,"ajaxError",[n,i,t||e]),y(e,n,i)}function y(t,e,n){var i=n.context;n.complete.call(i,e,t),h(n,i,"ajaxComplete",[e,n]),d(n)}function x(){}function b(t){return t&&(t=t.split(";",2)[0]),t&&(t==f?"html":t==u?"json":s.test(t)?"script":a.test(t)&&"xml")||"text"}function w(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function E(e){e.processData&&e.data&&"string"!=t.type(e.data)&&(e.data=t.param(e.data,e.traditional)),!e.data||e.type&&"GET"!=e.type.toUpperCase()||(e.url=w(e.url,e.data),e.data=void 0)}function j(e,n,i,r){return t.isFunction(n)&&(r=i,i=n,n=void 0),t.isFunction(i)||(r=i,i=void 0),{url:e,data:n,success:i,dataType:r}}function S(e,n,i,r){var o,s=t.isArray(n),a=t.isPlainObject(n);t.each(n,function(n,u){o=t.type(u),r&&(n=i?r:r+"["+(a||"object"==o||"array"==o?n:"")+"]"),!r&&s?e.add(u.name,u.value):"array"==o||!i&&"object"==o?S(e,u,i,n):e.add(n,u)})}var i,r,e=0,n=window.document,o=/)<[^<]*)*<\/script>/gi,s=/^(?:text|application)\/javascript/i,a=/^(?:text|application)\/xml/i,u="application/json",f="text/html",c=/^\s*$/;t.active=0,t.ajaxJSONP=function(i,r){if(!("type"in i))return t.ajax(i);var f,h,o=i.jsonpCallback,s=(t.isFunction(o)?o():o)||"jsonp"+ ++e,a=n.createElement("script"),u=window[s],c=function(e){t(a).triggerHandler("error",e||"abort")},l={abort:c};return r&&r.promise(l),t(a).on("load error",function(e,n){clearTimeout(h),t(a).off().remove(),"error"!=e.type&&f?g(f[0],l,i,r):v(null,n||"error",l,i,r),window[s]=u,f&&t.isFunction(u)&&u(f[0]),u=f=void 0}),m(l,i)===!1?(c("abort"),l):(window[s]=function(){f=arguments},a.src=i.url.replace(/\?(.+)=\?/,"?$1="+s),n.head.appendChild(a),i.timeout>0&&(h=setTimeout(function(){c("timeout")},i.timeout)),l)},t.ajaxSettings={type:"GET",beforeSend:x,success:x,error:x,complete:x,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript, application/x-javascript",json:u,xml:"application/xml, text/xml",html:f,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},t.ajax=function(e){var n=t.extend({},e||{}),o=t.Deferred&&t.Deferred();for(i in t.ajaxSettings)void 0===n[i]&&(n[i]=t.ajaxSettings[i]);p(n),n.crossDomain||(n.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(n.url)&&RegExp.$2!=window.location.host),n.url||(n.url=window.location.toString()),E(n),n.cache===!1&&(n.url=w(n.url,"_="+Date.now()));var s=n.dataType,a=/\?.+=\?/.test(n.url);if("jsonp"==s||a)return a||(n.url=w(n.url,n.jsonp?n.jsonp+"=?":n.jsonp===!1?"":"callback=?")),t.ajaxJSONP(n,o);var j,u=n.accepts[s],f={},l=function(t,e){f[t.toLowerCase()]=[t,e]},h=/^([\w-]+:)\/\//.test(n.url)?RegExp.$1:window.location.protocol,d=n.xhr(),y=d.setRequestHeader;if(o&&o.promise(d),n.crossDomain||l("X-Requested-With","XMLHttpRequest"),l("Accept",u||"*/*"),(u=n.mimeType||u)&&(u.indexOf(",")>-1&&(u=u.split(",",2)[0]),d.overrideMimeType&&d.overrideMimeType(u)),(n.contentType||n.contentType!==!1&&n.data&&"GET"!=n.type.toUpperCase())&&l("Content-Type",n.contentType||"application/x-www-form-urlencoded"),n.headers)for(r in n.headers)l(r,n.headers[r]);if(d.setRequestHeader=l,d.onreadystatechange=function(){if(4==d.readyState){d.onreadystatechange=x,clearTimeout(j);var e,i=!1;if(d.status>=200&&d.status<300||304==d.status||0==d.status&&"file:"==h){s=s||b(n.mimeType||d.getResponseHeader("content-type")),e=d.responseText;try{"script"==s?(1,eval)(e):"xml"==s?e=d.responseXML:"json"==s&&(e=c.test(e)?null:t.parseJSON(e))}catch(r){i=r}i?v(i,"parsererror",d,n,o):g(e,d,n,o)}else v(d.statusText||null,d.status?"error":"abort",d,n,o)}},m(d,n)===!1)return d.abort(),v(null,"abort",d,n,o),d;if(n.xhrFields)for(r in n.xhrFields)d[r]=n.xhrFields[r];var T="async"in n?n.async:!0;d.open(n.type,n.url,T,n.username,n.password);for(r in f)y.apply(d,f[r]);return n.timeout>0&&(j=setTimeout(function(){d.onreadystatechange=x,d.abort(),v(null,"timeout",d,n,o)},n.timeout)),d.send(n.data?n.data:null),d},t.get=function(){return t.ajax(j.apply(null,arguments))},t.post=function(){var e=j.apply(null,arguments);return e.type="POST",t.ajax(e)},t.getJSON=function(){var e=j.apply(null,arguments);return e.dataType="json",t.ajax(e)},t.fn.load=function(e,n,i){if(!this.length)return this;var a,r=this,s=e.split(/\s/),u=j(e,n,i),f=u.success;return s.length>1&&(u.url=s[0],a=s[1]),u.success=function(e){r.html(a?t("
").html(e.replace(o,"")).find(a):e),f&&f.apply(r,arguments)},t.ajax(u),this};var T=encodeURIComponent;t.param=function(t,e){var n=[];return n.add=function(t,e){this.push(T(t)+"="+T(e))},S(n,t,e),n.join("&").replace(/%20/g,"+")}}(Zepto),function(t){t.fn.serializeArray=function(){var n,e=[];return t([].slice.call(this.get(0).elements)).each(function(){n=t(this);var i=n.attr("type");"fieldset"!=this.nodeName.toLowerCase()&&!this.disabled&&"submit"!=i&&"reset"!=i&&"button"!=i&&("radio"!=i&&"checkbox"!=i||this.checked)&&e.push({name:n.attr("name"),value:n.val()})}),e},t.fn.serialize=function(){var t=[];return this.serializeArray().forEach(function(e){t.push(encodeURIComponent(e.name)+"="+encodeURIComponent(e.value))}),t.join("&")},t.fn.submit=function(e){if(e)this.bind("submit",e);else if(this.length){var n=t.Event("submit");this.eq(0).trigger(n),n.isDefaultPrevented()||this.get(0).submit()}return this}}(Zepto),function(t){"__proto__"in{}||t.extend(t.zepto,{Z:function(e,n){return e=e||[],t.extend(e,t.fn),e.selector=n||"",e.__Z=!0,e},isZ:function(e){return"array"===t.type(e)&&"__Z"in e}});try{getComputedStyle(void 0)}catch(e){var n=getComputedStyle;window.getComputedStyle=function(t){try{return n(t)}catch(e){return null}}}}(Zepto); Whoops/Resources/js/whoops.base.js000064400000013157150251301650013204 0ustar00Zepto(function($) { var $leftPanel = $('.left-panel'); var $frameContainer = $('.frames-container'); var $appFramesTab = $('#application-frames-tab'); var $allFramesTab = $('#all-frames-tab'); var $container = $('.details-container'); var $activeLine = $frameContainer.find('.frame.active'); var $activeFrame = $container.find('.frame-code.active'); var $ajaxEditors = $('.editor-link[data-ajax]'); var $header = $('header'); $header.on('mouseenter', function () { if ($header.find('.exception').height() >= 145) { $header.addClass('header-expand'); } }); $header.on('mouseleave', function () { $header.removeClass('header-expand'); }); /* * add prettyprint classes to our current active codeblock * run prettyPrint() to highlight the active code * scroll to the line when prettyprint is done * highlight the current line */ var renderCurrentCodeblock = function(id) { Prism.highlightAll(); highlightCurrentLine(); } /* * Highlight the active and neighboring lines for the current frame * Adjust the offset to make sure that line is veritcally centered */ var highlightCurrentLine = function() { // We show more code than needed, purely for proper syntax highlighting // Let’s hide a big chunk of that code and then scroll the remaining block $activeFrame.find('.code-block').first().css({ maxHeight: 345, overflow: 'hidden', }); var line = $activeFrame.find('.code-block .line-highlight').first()[0]; line.scrollIntoView(); line.parentElement.scrollTop -= 180; $container.scrollTop(0); } /* * click handler for loading codeblocks */ $frameContainer.on('click', '.frame', function() { var $this = $(this); var id = /frame\-line\-([\d]*)/.exec($this.attr('id'))[1]; var $codeFrame = $('#frame-code-' + id); if ($codeFrame) { $activeLine.removeClass('active'); $activeFrame.removeClass('active'); $this.addClass('active'); $codeFrame.addClass('active'); $activeLine = $this; $activeFrame = $codeFrame; renderCurrentCodeblock(id); } }); var clipboard = new Clipboard('.clipboard'); var showTooltip = function(elem, msg) { elem.classList.add('tooltipped', 'tooltipped-s'); elem.setAttribute('aria-label', msg); }; clipboard.on('success', function(e) { e.clearSelection(); showTooltip(e.trigger, 'Copied!'); }); clipboard.on('error', function(e) { showTooltip(e.trigger, fallbackMessage(e.action)); }); var btn = document.querySelector('.clipboard'); btn.addEventListener('mouseleave', function(e) { e.currentTarget.classList.remove('tooltipped', 'tooltipped-s'); e.currentTarget.removeAttribute('aria-label'); }); function fallbackMessage(action) { var actionMsg = ''; var actionKey = (action === 'cut' ? 'X' : 'C'); if (/Mac/i.test(navigator.userAgent)) { actionMsg = 'Press ⌘-' + actionKey + ' to ' + action; } else { actionMsg = 'Press Ctrl-' + actionKey + ' to ' + action; } return actionMsg; } function scrollIntoView($node, $parent) { var nodeOffset = $node.offset(); var nodeTop = nodeOffset.top; var nodeBottom = nodeTop + nodeOffset.height; var parentScrollTop = $parent.scrollTop(); var parentHeight = $parent.height(); if (nodeTop < 0) { $parent.scrollTop(parentScrollTop + nodeTop); } else if (nodeBottom > parentHeight) { $parent.scrollTop(parentScrollTop + nodeBottom - parentHeight); } } $(document).on('keydown', function(e) { var applicationFrames = $frameContainer.hasClass('frames-container-application'), frameClass = applicationFrames ? '.frame.frame-application' : '.frame'; if(e.ctrlKey || e.which === 74 || e.which === 75) { // CTRL+Arrow-UP/k and Arrow-Down/j support: // 1) select the next/prev element // 2) make sure the newly selected element is within the view-scope // 3) focus the (right) container, so arrow-up/down (without ctrl) scroll the details if (e.which === 38 /* arrow up */ || e.which === 75 /* k */) { $activeLine.prev(frameClass).click(); scrollIntoView($activeLine, $leftPanel); $container.focus(); e.preventDefault(); } else if (e.which === 40 /* arrow down */ || e.which === 74 /* j */) { $activeLine.next(frameClass).click(); scrollIntoView($activeLine, $leftPanel); $container.focus(); e.preventDefault(); } } else if (e.which == 78 /* n */) { if ($appFramesTab.length) { setActiveFramesTab($('.frames-tab:not(.frames-tab-active)')); } } }); // Render late enough for highlightCurrentLine to be ready renderCurrentCodeblock(); // Avoid to quit the page with some protocol (e.g. IntelliJ Platform REST API) $ajaxEditors.on('click', function(e){ e.preventDefault(); $.get(this.href); }); // Symfony VarDumper: Close the by default expanded objects $('.sf-dump-expanded') .removeClass('sf-dump-expanded') .addClass('sf-dump-compact'); $('.sf-dump-toggle span').html('▶'); // Make the given frames-tab active function setActiveFramesTab($tab) { $tab.addClass('frames-tab-active'); if ($tab.attr('id') == 'application-frames-tab') { $frameContainer.addClass('frames-container-application'); $allFramesTab.removeClass('frames-tab-active'); } else { $frameContainer.removeClass('frames-container-application'); $appFramesTab.removeClass('frames-tab-active'); } } $('a.frames-tab').on('click', function(e) { e.preventDefault(); setActiveFramesTab($(this)); }); }); Whoops/Resources/js/clipboard.min.js000064400000021114150251301650013465 0ustar00/*! * clipboard.js v1.5.3 * https://zenorocha.github.io/clipboard.js * * Licensed MIT © Zeno Rocha */ !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,r){function o(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return o(n?n:t)},l,l.exports,t,e,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;ar;r++)n[r].fn.apply(n[r].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),r=n[t],o=[];if(r&&e)for(var i=0,a=r.length;a>i;i++)r[i].fn!==e&&r[i].fn._!==e&&o.push(r[i]);return o.length?n[t]=o:delete n[t],this}},e.exports=r},{}],8:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}n.__esModule=!0;var i=function(){function t(t,e){for(var n=0;n */ namespace Whoops; use InvalidArgumentException; use Throwable; use Whoops\Exception\ErrorException; use Whoops\Handler\CallbackHandler; use Whoops\Handler\Handler; use Whoops\Handler\HandlerInterface; use Whoops\Inspector\CallableInspectorFactory; use Whoops\Inspector\InspectorFactory; use Whoops\Inspector\InspectorFactoryInterface; use Whoops\Inspector\InspectorInterface; use Whoops\Util\Misc; use Whoops\Util\SystemFacade; final class Run implements RunInterface { /** * @var bool */ private $isRegistered; /** * @var bool */ private $allowQuit = true; /** * @var bool */ private $sendOutput = true; /** * @var integer|false */ private $sendHttpCode = 500; /** * @var integer|false */ private $sendExitCode = 1; /** * @var HandlerInterface[] */ private $handlerStack = []; /** * @var array * @psalm-var list */ private $silencedPatterns = []; /** * @var SystemFacade */ private $system; /** * In certain scenarios, like in shutdown handler, we can not throw exceptions. * * @var bool */ private $canThrowExceptions = true; /** * The inspector factory to create inspectors. * * @var InspectorFactoryInterface */ private $inspectorFactory; /** * @var array */ private $frameFilters = []; public function __construct(SystemFacade $system = null) { $this->system = $system ?: new SystemFacade; $this->inspectorFactory = new InspectorFactory(); } /** * Explicitly request your handler runs as the last of all currently registered handlers. * * @param callable|HandlerInterface $handler * * @return Run */ public function appendHandler($handler) { array_unshift($this->handlerStack, $this->resolveHandler($handler)); return $this; } /** * Explicitly request your handler runs as the first of all currently registered handlers. * * @param callable|HandlerInterface $handler * * @return Run */ public function prependHandler($handler) { return $this->pushHandler($handler); } /** * Register your handler as the last of all currently registered handlers (to be executed first). * Prefer using appendHandler and prependHandler for clarity. * * @param callable|HandlerInterface $handler * * @return Run * * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface. */ public function pushHandler($handler) { $this->handlerStack[] = $this->resolveHandler($handler); return $this; } /** * Removes and returns the last handler pushed to the handler stack. * * @see Run::removeFirstHandler(), Run::removeLastHandler() * * @return HandlerInterface|null */ public function popHandler() { return array_pop($this->handlerStack); } /** * Removes the first handler. * * @return void */ public function removeFirstHandler() { array_pop($this->handlerStack); } /** * Removes the last handler. * * @return void */ public function removeLastHandler() { array_shift($this->handlerStack); } /** * Returns an array with all handlers, in the order they were added to the stack. * * @return array */ public function getHandlers() { return $this->handlerStack; } /** * Clears all handlers in the handlerStack, including the default PrettyPage handler. * * @return Run */ public function clearHandlers() { $this->handlerStack = []; return $this; } public function getFrameFilters() { return $this->frameFilters; } public function clearFrameFilters() { $this->frameFilters = []; return $this; } /** * Registers this instance as an error handler. * * @return Run */ public function register() { if (!$this->isRegistered) { // Workaround PHP bug 42098 // https://bugs.php.net/bug.php?id=42098 class_exists("\\Whoops\\Exception\\ErrorException"); class_exists("\\Whoops\\Exception\\FrameCollection"); class_exists("\\Whoops\\Exception\\Frame"); class_exists("\\Whoops\\Exception\\Inspector"); class_exists("\\Whoops\\Inspector\\InspectorFactory"); $this->system->setErrorHandler([$this, self::ERROR_HANDLER]); $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]); $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]); $this->isRegistered = true; } return $this; } /** * Unregisters all handlers registered by this Whoops\Run instance. * * @return Run */ public function unregister() { if ($this->isRegistered) { $this->system->restoreExceptionHandler(); $this->system->restoreErrorHandler(); $this->isRegistered = false; } return $this; } /** * Should Whoops allow Handlers to force the script to quit? * * @param bool|int $exit * * @return bool */ public function allowQuit($exit = null) { if (func_num_args() == 0) { return $this->allowQuit; } return $this->allowQuit = (bool) $exit; } /** * Silence particular errors in particular files. * * @param array|string $patterns List or a single regex pattern to match. * @param int $levels Defaults to E_STRICT | E_DEPRECATED. * * @return Run */ public function silenceErrorsInPaths($patterns, $levels = 10240) { $this->silencedPatterns = array_merge( $this->silencedPatterns, array_map( function ($pattern) use ($levels) { return [ "pattern" => $pattern, "levels" => $levels, ]; }, (array) $patterns ) ); return $this; } /** * Returns an array with silent errors in path configuration. * * @return array */ public function getSilenceErrorsInPaths() { return $this->silencedPatterns; } /** * Should Whoops send HTTP error code to the browser if possible? * Whoops will by default send HTTP code 500, but you may wish to * use 502, 503, or another 5xx family code. * * @param bool|int $code * * @return int|false * * @throws InvalidArgumentException */ public function sendHttpCode($code = null) { if (func_num_args() == 0) { return $this->sendHttpCode; } if (!$code) { return $this->sendHttpCode = false; } if ($code === true) { $code = 500; } if ($code < 400 || 600 <= $code) { throw new InvalidArgumentException( "Invalid status code '$code', must be 4xx or 5xx" ); } return $this->sendHttpCode = $code; } /** * Should Whoops exit with a specific code on the CLI if possible? * Whoops will exit with 1 by default, but you can specify something else. * * @param int $code * * @return int * * @throws InvalidArgumentException */ public function sendExitCode($code = null) { if (func_num_args() == 0) { return $this->sendExitCode; } if ($code < 0 || 255 <= $code) { throw new InvalidArgumentException( "Invalid status code '$code', must be between 0 and 254" ); } return $this->sendExitCode = (int) $code; } /** * Should Whoops push output directly to the client? * If this is false, output will be returned by handleException. * * @param bool|int $send * * @return bool */ public function writeToOutput($send = null) { if (func_num_args() == 0) { return $this->sendOutput; } return $this->sendOutput = (bool) $send; } /** * Handles an exception, ultimately generating a Whoops error page. * * @param Throwable $exception * * @return string Output generated by handlers. */ public function handleException($exception) { // Walk the registered handlers in the reverse order // they were registered, and pass off the exception $inspector = $this->getInspector($exception); // Capture output produced while handling the exception, // we might want to send it straight away to the client, // or return it silently. $this->system->startOutputBuffering(); // Just in case there are no handlers: $handlerResponse = null; $handlerContentType = null; try { foreach (array_reverse($this->handlerStack) as $handler) { $handler->setRun($this); $handler->setInspector($inspector); $handler->setException($exception); // The HandlerInterface does not require an Exception passed to handle() // and neither of our bundled handlers use it. // However, 3rd party handlers may have already relied on this parameter, // and removing it would be possibly breaking for users. $handlerResponse = $handler->handle($exception); // Collect the content type for possible sending in the headers. $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null; if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) { // The Handler has handled the exception in some way, and // wishes to quit execution (Handler::QUIT), or skip any // other handlers (Handler::LAST_HANDLER). If $this->allowQuit // is false, Handler::QUIT behaves like Handler::LAST_HANDLER break; } } $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit(); } finally { $output = $this->system->cleanOutputBuffer(); } // If we're allowed to, send output generated by handlers directly // to the output, otherwise, and if the script doesn't quit, return // it so that it may be used by the caller if ($this->writeToOutput()) { // @todo Might be able to clean this up a bit better if ($willQuit) { // Cleanup all other output buffers before sending our output: while ($this->system->getOutputBufferLevel() > 0) { $this->system->endOutputBuffering(); } // Send any headers if needed: if (Misc::canSendHeaders() && $handlerContentType) { header("Content-Type: {$handlerContentType}"); } } $this->writeToOutputNow($output); } if ($willQuit) { // HHVM fix for https://github.com/facebook/hhvm/issues/4055 $this->system->flushOutputBuffer(); $this->system->stopExecution( $this->sendExitCode() ); } return $output; } /** * Converts generic PHP errors to \ErrorException instances, before passing them off to be handled. * * This method MUST be compatible with set_error_handler. * * @param int $level * @param string $message * @param string|null $file * @param int|null $line * * @return bool * * @throws ErrorException */ public function handleError($level, $message, $file = null, $line = null) { if ($level & $this->system->getErrorReportingLevel()) { foreach ($this->silencedPatterns as $entry) { $pathMatches = (bool) preg_match($entry["pattern"], $file); $levelMatches = $level & $entry["levels"]; if ($pathMatches && $levelMatches) { // Ignore the error, abort handling // See https://github.com/filp/whoops/issues/418 return true; } } // XXX we pass $level for the "code" param only for BC reasons. // see https://github.com/filp/whoops/issues/267 $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line); if ($this->canThrowExceptions) { throw $exception; } else { $this->handleException($exception); } // Do not propagate errors which were already handled by Whoops. return true; } // Propagate error to the next handler, allows error_get_last() to // work on silenced errors. return false; } /** * Special case to deal with Fatal errors and the like. * * @return void */ public function handleShutdown() { // If we reached this step, we are in shutdown handler. // An exception thrown in a shutdown handler will not be propagated // to the exception handler. Pass that information along. $this->canThrowExceptions = false; $error = $this->system->getLastError(); if ($error && Misc::isLevelFatal($error['type'])) { // If there was a fatal error, // it was not handled in handleError yet. $this->allowQuit = false; $this->handleError( $error['type'], $error['message'], $error['file'], $error['line'] ); } } /** * @param InspectorFactoryInterface $factory * * @return void */ public function setInspectorFactory(InspectorFactoryInterface $factory) { $this->inspectorFactory = $factory; } public function addFrameFilter($filterCallback) { if (!is_callable($filterCallback)) { throw new \InvalidArgumentException(sprintf( "A frame filter must be of type callable, %s type given.", gettype($filterCallback) )); } $this->frameFilters[] = $filterCallback; return $this; } /** * @param Throwable $exception * * @return InspectorInterface */ private function getInspector($exception) { return $this->inspectorFactory->create($exception); } /** * Resolves the giving handler. * * @param callable|HandlerInterface $handler * * @return HandlerInterface * * @throws InvalidArgumentException */ private function resolveHandler($handler) { if (is_callable($handler)) { $handler = new CallbackHandler($handler); } if (!$handler instanceof HandlerInterface) { throw new InvalidArgumentException( "Handler must be a callable, or instance of " . "Whoops\\Handler\\HandlerInterface" ); } return $handler; } /** * Echo something to the browser. * * @param string $output * * @return Run */ private function writeToOutputNow($output) { if ($this->sendHttpCode() && Misc::canSendHeaders()) { $this->system->setHttpResponseCode( $this->sendHttpCode() ); } echo $output; return $this; } } Console/InstallCommand.php000064400000003230150251305720011562 0ustar00option('with')) { $services = $this->option('with') == 'none' ? [] : explode(',', $this->option('with')); } elseif ($this->option('no-interaction')) { $services = $this->defaultServices; } else { $services = $this->gatherServicesWithSymfonyMenu(); } if ($invalidServices = array_diff($services, $this->services)) { $this->error('Invalid services ['.implode(',', $invalidServices).'].'); return 1; } $this->buildDockerCompose($services); $this->replaceEnvVariables($services); $this->configurePhpUnit(); if ($this->option('devcontainer')) { $this->installDevContainer(); } $this->info('Sail scaffolding installed successfully.'); $this->prepareInstallation($services); } } Console/AddCommand.php000064400000002731150251305720010651 0ustar00argument('services')) { $services = $this->argument('services') == 'none' ? [] : explode(',', $this->argument('services')); } elseif ($this->option('no-interaction')) { $services = $this->defaultServices; } else { $services = $this->gatherServicesWithSymfonyMenu(); } if ($invalidServices = array_diff($services, $this->services)) { $this->error('Invalid services ['.implode(',', $invalidServices).'].'); return 1; } $this->buildDockerCompose($services); $this->replaceEnvVariables($services); $this->configurePhpUnit(); $this->info('Additional Sail services installed successfully.'); $this->prepareInstallation($services); } } Console/PublishCommand.php000064400000002360150251305720011565 0ustar00call('vendor:publish', ['--tag' => 'sail-docker']); file_put_contents( $this->laravel->basePath('docker-compose.yml'), str_replace( [ './vendor/laravel/sail/runtimes/8.2', './vendor/laravel/sail/runtimes/8.1', './vendor/laravel/sail/runtimes/8.0', './vendor/laravel/sail/runtimes/7.4', ], [ './docker/8.2', './docker/8.1', './docker/8.0', './docker/7.4', ], file_get_contents($this->laravel->basePath('docker-compose.yml')) ) ); } } Console/Concerns/InteractsWithDockerComposeServices.php000064400000021624150251305720017410 0ustar00 */ protected $services = [ 'mysql', 'pgsql', 'mariadb', 'redis', 'memcached', 'meilisearch', 'minio', 'mailpit', 'selenium', 'soketi', ]; /** * The default services used when the user chooses non-interactive mode. * * @var string[] */ protected $defaultServices = ['mysql', 'redis', 'selenium', 'mailpit']; /** * Gather the desired Sail services using a Symfony menu. * * @return array */ protected function gatherServicesWithSymfonyMenu() { return $this->choice('Which services would you like to install?', $this->services, 0, null, true); } /** * Build the Docker Compose file. * * @param array $services * @return void */ protected function buildDockerCompose(array $services) { $composePath = base_path('docker-compose.yml'); $compose = file_exists($composePath) ? Yaml::parseFile($composePath) : Yaml::parse(file_get_contents(__DIR__ . '/../../../stubs/docker-compose.stub')); // Adds the new services as dependencies of the laravel.test service... if (! array_key_exists('laravel.test', $compose['services'])) { $this->warn('Couldn\'t find the laravel.test service. Make sure you add ['.implode(',', $services).'] to the depends_on config.'); } else { $compose['services']['laravel.test']['depends_on'] = collect($compose['services']['laravel.test']['depends_on'] ?? []) ->merge($services) ->unique() ->values() ->all(); } // Add the services to the docker-compose.yml... collect($services) ->filter(function ($service) use ($compose) { return ! array_key_exists($service, $compose['services'] ?? []); })->each(function ($service) use (&$compose) { $compose['services'][$service] = Yaml::parseFile(__DIR__ . "/../../../stubs/{$service}.stub")[$service]; }); // Merge volumes... collect($services) ->filter(function ($service) { return in_array($service, ['mysql', 'pgsql', 'mariadb', 'redis', 'meilisearch', 'minio']); })->filter(function ($service) use ($compose) { return ! array_key_exists($service, $compose['volumes'] ?? []); })->each(function ($service) use (&$compose) { $compose['volumes']["sail-{$service}"] = ['driver' => 'local']; }); // If the list of volumes is empty, we can remove it... if (empty($compose['volumes'])) { unset($compose['volumes']); } // Replace Selenium with ARM base container on Apple Silicon... if (in_array('selenium', $services) && in_array(php_uname('m'), ['arm64', 'aarch64'])) { $compose['services']['selenium']['image'] = 'seleniarm/standalone-chromium'; } file_put_contents($this->laravel->basePath('docker-compose.yml'), Yaml::dump($compose, Yaml::DUMP_OBJECT_AS_MAP)); } /** * Replace the Host environment variables in the app's .env file. * * @param array $services * @return void */ protected function replaceEnvVariables(array $services) { $environment = file_get_contents($this->laravel->basePath('.env')); if (in_array('pgsql', $services)) { $environment = str_replace('DB_CONNECTION=mysql', "DB_CONNECTION=pgsql", $environment); $environment = str_replace('DB_HOST=127.0.0.1', "DB_HOST=pgsql", $environment); $environment = str_replace('DB_PORT=3306', "DB_PORT=5432", $environment); } elseif (in_array('mariadb', $services)) { $environment = str_replace('DB_HOST=127.0.0.1', "DB_HOST=mariadb", $environment); } else { $environment = str_replace('DB_HOST=127.0.0.1', "DB_HOST=mysql", $environment); } $environment = str_replace('DB_USERNAME=root', "DB_USERNAME=sail", $environment); $environment = preg_replace("/DB_PASSWORD=(.*)/", "DB_PASSWORD=password", $environment); if (in_array('memcached', $services)) { $environment = str_replace('MEMCACHED_HOST=127.0.0.1', 'MEMCACHED_HOST=memcached', $environment); } if (in_array('redis', $services)) { $environment = str_replace('REDIS_HOST=127.0.0.1', 'REDIS_HOST=redis', $environment); } if (in_array('meilisearch', $services)) { $environment .= "\nSCOUT_DRIVER=meilisearch"; $environment .= "\nMEILISEARCH_HOST=http://meilisearch:7700\n"; } if (in_array('soketi', $services)) { $environment = preg_replace("/^BROADCAST_DRIVER=(.*)/m", "BROADCAST_DRIVER=pusher", $environment); $environment = preg_replace("/^PUSHER_APP_ID=(.*)/m", "PUSHER_APP_ID=app-id", $environment); $environment = preg_replace("/^PUSHER_APP_KEY=(.*)/m", "PUSHER_APP_KEY=app-key", $environment); $environment = preg_replace("/^PUSHER_APP_SECRET=(.*)/m", "PUSHER_APP_SECRET=app-secret", $environment); $environment = preg_replace("/^PUSHER_HOST=(.*)/m", "PUSHER_HOST=soketi", $environment); $environment = preg_replace("/^PUSHER_PORT=(.*)/m", "PUSHER_PORT=6001", $environment); $environment = preg_replace("/^PUSHER_SCHEME=(.*)/m", "PUSHER_SCHEME=http", $environment); $environment = preg_replace("/^VITE_PUSHER_HOST=(.*)/m", "VITE_PUSHER_HOST=localhost", $environment); } if (in_array('mailpit', $services)) { $environment = preg_replace("/^MAIL_HOST=(.*)/m", "MAIL_HOST=mailpit", $environment); } file_put_contents($this->laravel->basePath('.env'), $environment); } /** * Configure PHPUnit to use the dedicated testing database. * * @return void */ protected function configurePhpUnit() { if (! file_exists($path = $this->laravel->basePath('phpunit.xml'))) { $path = $this->laravel->basePath('phpunit.xml.dist'); } $phpunit = file_get_contents($path); $phpunit = preg_replace('/^.*DB_CONNECTION.*\n/m', '', $phpunit); $phpunit = str_replace('', '', $phpunit); file_put_contents($this->laravel->basePath('phpunit.xml'), $phpunit); } /** * Install the devcontainer.json configuration file. * * @return void */ protected function installDevContainer() { if (! is_dir($this->laravel->basePath('.devcontainer'))) { mkdir($this->laravel->basePath('.devcontainer'), 0755, true); } file_put_contents( $this->laravel->basePath('.devcontainer/devcontainer.json'), file_get_contents(__DIR__.'/../../../stubs/devcontainer.stub') ); $environment = file_get_contents($this->laravel->basePath('.env')); $environment .= "\nWWWGROUP=1000"; $environment .= "\nWWWUSER=1000\n"; file_put_contents($this->laravel->basePath('.env'), $environment); } /** * Prepare the installation by pulling and building any necessary images. * * @param array $services * @return void */ protected function prepareInstallation($services) { // Ensure docker is installed... if ($this->runCommands(['docker info > /dev/null 2>&1']) !== 0) { return; } if (count($services) > 0) { $status = $this->runCommands([ './vendor/bin/sail pull '.implode(' ', $services), ]); if ($status === 0) { $this->info('Sail images installed successfully.'); } } $status = $this->runCommands([ './vendor/bin/sail build', ]); if ($status === 0) { $this->info('Sail build successful.'); } } /** * Run the given commands. * * @param array $commands * @return int */ protected function runCommands($commands) { $process = Process::fromShellCommandline(implode(' && ', $commands), null, null, null, null); if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { try { $process->setTty(true); } catch (\RuntimeException $e) { $this->output->writeln(' WARN '.$e->getMessage().PHP_EOL); } } return $process->run(function ($type, $line) { $this->output->write(' '.$line); }); } } SailServiceProvider.php000064400000003130150251305720011176 0ustar00registerCommands(); $this->configurePublishing(); } /** * Register the console commands for the package. * * @return void */ protected function registerCommands() { if ($this->app->runningInConsole()) { $this->commands([ InstallCommand::class, AddCommand::class, PublishCommand::class, ]); } } /** * Configure publishing for the package. * * @return void */ protected function configurePublishing() { if ($this->app->runningInConsole()) { $this->publishes([ __DIR__ . '/../runtimes' => $this->app->basePath('docker'), ], ['sail', 'sail-docker']); $this->publishes([ __DIR__ . '/../bin/sail' => $this->app->basePath('sail'), ], ['sail', 'sail-bin']); } } /** * Get the services provided by the provider. * * @return array */ public function provides() { return [ InstallCommand::class, PublishCommand::class, ]; } } Internal/Calculator/NativeCalculator.php000064400000032665150251432120014371 0ustar00maxDigits = 9; break; case 8: $this->maxDigits = 18; break; default: throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.'); } } 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; } public function sub(string $a, string $b) : string { return $this->add($a, $this->neg($b)); } 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; } public function divQ(string $a, string $b) : string { return $this->divQR($a, $b)[0]; } public function divR(string $a, string $b): string { return $this->divQR($a, $b)[1]; } 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]; } 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/ */ 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 */ 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. */ 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. */ 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. */ 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. * * @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. * * @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. * * @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.php000064400000004420150251432120013652 0ustar00init($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. */ abstract public function add(string $a, string $b) : string; /** * Subtracts two numbers. */ abstract public function sub(string $a, string $b) : string; /** * Multiplies two numbers. */ 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 array{string, 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 $b The modulus; must not be zero. */ 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 $m The modulus; must not be negative or zero. */ 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); } [$g, $x] = $this->gcdExtended($modVal, $m); 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. */ 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. * * @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)); } /** * @return array{string, string, string} GCD, X, Y */ private function gcdExtended(string $a, string $b) : array { if ($a === '0') { return [$b, '0', '1']; } [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a); $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1)); $y = $x1; return [$gcd, $x, $y]; } /** * 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. */ 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. * * @throws \InvalidArgumentException If the rounding mode is invalid. * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary. * * @psalm-suppress ImpureFunctionCall */ 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. */ 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. */ 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. */ public function xor(string $a, string $b) : string { return $this->bitwise('xor', $a, $b); } /** * Performs a bitwise operation on a decimal number. * * @param 'and'|'or'|'xor' $operator The operator to use. * @param string $a The left operand. * @param string $b The right operand. */ 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. */ 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. */ 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. */ 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.php000064400000053505150251432120007240 0ustar00value = $value; $this->scale = $scale; } /** * Creates a BigDecimal of the given value. * * @throws MathException If the value cannot be converted to a BigDecimal. * * @psalm-pure */ public static function of(BigNumber|int|float|string $value) : BigDecimal { 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. * * @throws \InvalidArgumentException If the scale is negative. * * @psalm-pure */ public static function ofUnscaledValue(BigNumber|int|float|string $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. * * @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. * * @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. * * @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. * * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. */ public function plus(BigNumber|int|float|string $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. * * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. */ public function minus(BigNumber|int|float|string $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. * * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. */ public function multipliedBy(BigNumber|int|float|string $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. * * @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(BigNumber|int|float|string $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. * * @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(BigNumber|int|float|string $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`. * * @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. * * @throws MathException If the divisor is not a valid decimal number, or is zero. */ public function quotient(BigNumber|int|float|string $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. * * @throws MathException If the divisor is not a valid decimal number, or is zero. */ public function remainder(BigNumber|int|float|string $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(BigNumber|int|float|string $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. * * @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. */ 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. */ 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. */ 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. */ public function abs() : BigDecimal { return $this->isNegative() ? $this->negated() : $this; } /** * Returns the negated value of this number. */ public function negated() : BigDecimal { return new BigDecimal(Calculator::get()->neg($this->value), $this->scale); } public function compareTo(BigNumber|int|float|string $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); } public function getSign() : int { return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); } public function getUnscaledValue() : BigInteger { return self::newBigInteger($this->value); } public function getScale() : int { return $this->scale; } /** * Returns a string representing the integral part of this decimal number. * * Example: `-123.456` => `-123`. */ 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` => ''. */ 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. */ public function hasNonZeroFractionalPart() : bool { return $this->getFractionalPart() !== \str_repeat('0', $this->scale); } public function toBigInteger() : BigInteger { $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); return self::newBigInteger($zeroScaleDecimal->value); } public function toBigDecimal() : BigDecimal { return $this; } public function toBigRational() : BigRational { $numerator = self::newBigInteger($this->value); $denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale)); return self::newBigRational($numerator, $denominator, false); } public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { if ($scale === $this->scale) { return $this; } return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode); } public function toInt() : int { return $this->toBigInteger()->toInt(); } public function toFloat() : float { return (float) (string) $this; } 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 * * @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 */ 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 * * @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. * * @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]; } 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. */ 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.php000064400000036361150251432120007133 0ustar00[\-\+])?' . '(?:' . '(?:' . '(?[0-9]+)?' . '(?\.)?' . '(?[0-9]+)?' . '(?:[eE](?[\-\+]?[0-9]+))?' . ')|(?:' . '(?[0-9]+)' . '\/?' . '(?[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 * * @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(BigNumber|int|float|string $value) : BigNumber { if ($value instanceof BigNumber) { return $value; } if (\is_int($value)) { return new BigInteger((string) $value); } $value = \is_float($value) ? self::floatToString($value) : $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 fn(string $value): ?string => (($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 * * @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 BigInteger's protected constructor from sibling classes. * * @internal * @psalm-pure */ protected function newBigInteger(string $value) : BigInteger { return new BigInteger($value); } /** * Proxy method to access BigDecimal's protected constructor from sibling classes. * * @internal * @psalm-pure */ protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal { return new BigDecimal($value, $scale); } /** * Proxy method to access BigRational's protected constructor from sibling classes. * * @internal * @psalm-pure */ protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational { return new BigRational($numerator, $denominator, $checkDenominator); } /** * 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. * * @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(BigNumber|int|float|string ...$values) : static { $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. * * @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(BigNumber|int|float|string ...$values) : static { $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. * * @throws \InvalidArgumentException If no values are given. * @throws MathException If an argument is not valid. * * @psalm-pure */ public static function sum(BigNumber|int|float|string ...$values) : static { /** @var static|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...) * * @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. * * @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. */ public function isEqualTo(BigNumber|int|float|string $that) : bool { return $this->compareTo($that) === 0; } /** * Checks if this number is strictly lower than the given one. */ public function isLessThan(BigNumber|int|float|string $that) : bool { return $this->compareTo($that) < 0; } /** * Checks if this number is lower than or equal to the given one. */ public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool { return $this->compareTo($that) <= 0; } /** * Checks if this number is strictly greater than the given one. */ public function isGreaterThan(BigNumber|int|float|string $that) : bool { return $this->compareTo($that) > 0; } /** * Checks if this number is greater than or equal to the given one. */ public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool { return $this->compareTo($that) >= 0; } /** * Checks if this number equals zero. */ public function isZero() : bool { return $this->getSign() === 0; } /** * Checks if this number is strictly negative. */ public function isNegative() : bool { return $this->getSign() < 0; } /** * Checks if this number is negative or zero. */ public function isNegativeOrZero() : bool { return $this->getSign() <= 0; } /** * Checks if this number is strictly positive. */ public function isPositive() : bool { return $this->getSign() > 0; } /** * Checks if this number is positive or zero. */ 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. * * @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(BigNumber|int|float|string $that) : int; /** * Converts this number to a BigInteger. * * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. */ abstract public function toBigInteger() : BigInteger; /** * Converts this number to a BigDecimal. * * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. */ abstract public function toBigDecimal() : BigDecimal; /** * Converts this number to a BigRational. */ 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. * * @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. * * @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. */ 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. */ abstract public function __toString() : string; public function jsonSerialize() : string { return $this->__toString(); } } BigRational.php000064400000031015150251432120007443 0ustar00isZero()) { 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. * * @throws MathException If the value cannot be converted to a BigRational. * * @psalm-pure */ public static function of(BigNumber|int|float|string $value) : BigRational { 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. * * @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( BigNumber|int|float|string $numerator, BigNumber|int|float|string $denominator, ) : BigRational { $numerator = BigInteger::of($numerator); $denominator = BigInteger::of($denominator); return new BigRational($numerator, $denominator, true); } /** * Returns a BigRational representing zero. * * @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. * * @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. * * @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; } public function getNumerator() : BigInteger { return $this->numerator; } public function getDenominator() : BigInteger { return $this->denominator; } /** * Returns the quotient of the division of the numerator by the denominator. */ public function quotient() : BigInteger { return $this->numerator->quotient($this->denominator); } /** * Returns the remainder of the division of the numerator by the denominator. */ 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. * * @throws MathException If the number is not valid. */ public function plus(BigNumber|int|float|string $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. * * @throws MathException If the number is not valid. */ public function minus(BigNumber|int|float|string $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. * * @throws MathException If the multiplier is not a valid number. */ public function multipliedBy(BigNumber|int|float|string $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. * * @throws MathException If the divisor is not a valid number, or is zero. */ public function dividedBy(BigNumber|int|float|string $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. * * @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. * * @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. */ public function abs() : BigRational { return new BigRational($this->numerator->abs(), $this->denominator, false); } /** * Returns the negated value of this BigRational. */ public function negated() : BigRational { return new BigRational($this->numerator->negated(), $this->denominator, false); } /** * Returns the simplified value of this 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); } public function compareTo(BigNumber|int|float|string $that) : int { return $this->minus($that)->getSign(); } public function getSign() : int { return $this->numerator->getSign(); } 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; } public function toBigDecimal() : BigDecimal { return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); } public function toBigRational() : BigRational { return $this; } public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); } public function toInt() : int { return $this->toBigInteger()->toInt(); } public function toFloat() : float { $simplified = $this->simplified(); return $simplified->numerator->toFloat() / $simplified->denominator->toFloat(); } 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 * * @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 */ 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 * * @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.php000064400000001016150251432120014204 0ustar00 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.php000064400000001340150251432120014002 0ustar00value = $value; } /** * Creates a BigInteger of the given value. * * @throws MathException If the value cannot be converted to a BigInteger. * * @psalm-pure */ public static function of(BigNumber|int|float|string $value) : BigInteger { 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. * * @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. * * @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. * * @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)|null $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. * * @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. * * @throws MathException If one of the parameters cannot be converted to a BigInteger, * or `$min` is greater than `$max`. */ public static function randomRange( BigNumber|int|float|string $min, BigNumber|int|float|string $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. * * @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. * * @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. * * @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; } public static function gcdMultiple(BigInteger $a, BigInteger ...$n): BigInteger { $result = $a; foreach ($n as $next) { $result = $result->gcd($next); if ($result->isEqualTo(1)) { return $result; } } return $result; } /** * 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. * * @throws MathException If the number is not valid, or is not convertible to a BigInteger. */ public function plus(BigNumber|int|float|string $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. * * @throws MathException If the number is not valid, or is not convertible to a BigInteger. */ public function minus(BigNumber|int|float|string $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. * * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger. */ public function multipliedBy(BigNumber|int|float|string $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. * * @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(BigNumber|int|float|string $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. * * @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. * * @throws DivisionByZeroException If the divisor is zero. */ public function quotient(BigNumber|int|float|string $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. * * @throws DivisionByZeroException If the divisor is zero. */ public function remainder(BigNumber|int|float|string $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(BigNumber|int|float|string $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. * * @throws DivisionByZeroException If the divisor is zero. */ public function mod(BigNumber|int|float|string $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. * * @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. * * @throws NegativeNumberException If any of the operands is negative. * @throws DivisionByZeroException If the modulus is zero. */ public function modPow(BigNumber|int|float|string $exp, BigNumber|int|float|string $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. */ public function gcd(BigNumber|int|float|string $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. * * @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. */ public function abs() : BigInteger { return $this->isNegative() ? $this->negated() : $this; } /** * Returns the inverse of this number. */ 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. */ public function and(BigNumber|int|float|string $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. */ public function or(BigNumber|int|float|string $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. */ public function xor(BigNumber|int|float|string $that) : BigInteger { $that = BigInteger::of($that); return new BigInteger(Calculator::get()->xor($this->value, $that->value)); } /** * Returns the bitwise-not of this BigInteger. */ public function not() : BigInteger { return $this->negated()->minus(1); } /** * Returns the integer left shifted by a given number of bits. */ 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. */ 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))). */ 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. */ 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. */ public function isEven() : bool { return \in_array($this->value[-1], ['0', '2', '4', '6', '8'], true); } /** * Returns whether this number is odd. */ 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<shiftedRight($n)->isOdd(); } public function compareTo(BigNumber|int|float|string $that) : int { $that = BigNumber::of($that); if ($that instanceof BigInteger) { return Calculator::get()->cmp($this->value, $that->value); } return - $that->compareTo($this); } public function getSign() : int { return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); } public function toBigInteger() : BigInteger { return $this; } public function toBigDecimal() : BigDecimal { return self::newBigDecimal($this->value); } public function toBigRational() : BigRational { return self::newBigRational($this, BigInteger::one(), false); } public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal { return $this->toBigDecimal()->toScale($scale, $roundingMode); } public function toInt() : int { $intValue = (int) $this->value; if ($this->value !== (string) $intValue) { throw IntegerOverflowException::toIntOverflow($this); } return $intValue; } 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. * * @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. * * @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. * * @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); } 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 * * @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 */ 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 * * @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.php000064400000007423150251432120007650 0ustar00= 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; } DegradedUuid.php000064400000001066150251436620007612 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; /** * @deprecated DegradedUuid is no longer necessary to represent UUIDs on 32-bit * systems. Transition typehints to {@see UuidInterface}. * * @psalm-immutable */ class DegradedUuid extends Uuid { } DeprecatedUuidMethodsTrait.php000064400000032600150251436620012501 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use DateTimeImmutable; use DateTimeInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Exception\DateTimeException; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Throwable; use function str_pad; use function substr; use const STR_PAD_LEFT; /** * This trait encapsulates deprecated methods for ramsey/uuid; this trait and * its methods will be removed in ramsey/uuid 5.0.0. * * @deprecated This trait and its methods will be removed in ramsey/uuid 5.0.0. * * @psalm-immutable */ trait DeprecatedUuidMethodsTrait { /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqHiAndReserved()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. */ public function getClockSeqHiAndReserved(): string { return $this->numberConverter->fromHex($this->fields->getClockSeqHiAndReserved()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqHiAndReserved()}. */ public function getClockSeqHiAndReservedHex(): string { return $this->fields->getClockSeqHiAndReserved()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqLow()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. */ public function getClockSeqLow(): string { return $this->numberConverter->fromHex($this->fields->getClockSeqLow()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqLow()}. */ public function getClockSeqLowHex(): string { return $this->fields->getClockSeqLow()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeq()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. */ public function getClockSequence(): string { return $this->numberConverter->fromHex($this->fields->getClockSeq()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeq()}. */ public function getClockSequenceHex(): string { return $this->fields->getClockSeq()->toString(); } /** * @deprecated This method will be removed in 5.0.0. There is no alternative * recommendation, so plan accordingly. */ public function getNumberConverter(): NumberConverterInterface { return $this->numberConverter; } /** * @deprecated In ramsey/uuid version 5.0.0, this will be removed. * It is available at {@see UuidV1::getDateTime()}. * * @return DateTimeImmutable An immutable instance of DateTimeInterface * * @throws UnsupportedOperationException if UUID is not time-based * @throws DateTimeException if DateTime throws an exception/error */ public function getDateTime(): DateTimeInterface { if ($this->fields->getVersion() !== 1) { throw new UnsupportedOperationException('Not a time-based UUID'); } $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); try { return new DateTimeImmutable( '@' . $time->getSeconds()->toString() . '.' . str_pad($time->getMicroseconds()->toString(), 6, '0', STR_PAD_LEFT) ); } catch (Throwable $e) { throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); } } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. * * @return string[] */ public function getFieldsHex(): array { return [ 'time_low' => $this->fields->getTimeLow()->toString(), 'time_mid' => $this->fields->getTimeMid()->toString(), 'time_hi_and_version' => $this->fields->getTimeHiAndVersion()->toString(), 'clock_seq_hi_and_reserved' => $this->fields->getClockSeqHiAndReserved()->toString(), 'clock_seq_low' => $this->fields->getClockSeqLow()->toString(), 'node' => $this->fields->getNode()->toString(), ]; } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getLeastSignificantBits(): string { $leastSignificantHex = substr($this->getHex()->toString(), 16); return $this->numberConverter->fromHex($leastSignificantHex); } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getLeastSignificantBitsHex(): string { return substr($this->getHex()->toString(), 16); } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getMostSignificantBits(): string { $mostSignificantHex = substr($this->getHex()->toString(), 0, 16); return $this->numberConverter->fromHex($mostSignificantHex); } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getMostSignificantBitsHex(): string { return substr($this->getHex()->toString(), 0, 16); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getNode()} and use the * arbitrary-precision math library of your choice to convert it to a * string integer. */ public function getNode(): string { return $this->numberConverter->fromHex($this->fields->getNode()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getNode()}. */ public function getNodeHex(): string { return $this->fields->getNode()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeHiAndVersion()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. */ public function getTimeHiAndVersion(): string { return $this->numberConverter->fromHex($this->fields->getTimeHiAndVersion()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeHiAndVersion()}. */ public function getTimeHiAndVersionHex(): string { return $this->fields->getTimeHiAndVersion()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeLow()} and use the * arbitrary-precision math library of your choice to convert it to a * string integer. */ public function getTimeLow(): string { return $this->numberConverter->fromHex($this->fields->getTimeLow()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeLow()}. */ public function getTimeLowHex(): string { return $this->fields->getTimeLow()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeMid()} and use the * arbitrary-precision math library of your choice to convert it to a * string integer. */ public function getTimeMid(): string { return $this->numberConverter->fromHex($this->fields->getTimeMid()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeMid()}. */ public function getTimeMidHex(): string { return $this->fields->getTimeMid()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimestamp()} and use * the arbitrary-precision math library of your choice to convert it to * a string integer. */ public function getTimestamp(): string { if ($this->fields->getVersion() !== 1) { throw new UnsupportedOperationException('Not a time-based UUID'); } return $this->numberConverter->fromHex($this->fields->getTimestamp()->toString()); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimestamp()}. */ public function getTimestampHex(): string { if ($this->fields->getVersion() !== 1) { throw new UnsupportedOperationException('Not a time-based UUID'); } return $this->fields->getTimestamp()->toString(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getVariant()}. */ public function getVariant(): ?int { return $this->fields->getVariant(); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see \Ramsey\Uuid\Fields\FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getVersion()}. */ public function getVersion(): ?int { return $this->fields->getVersion(); } } BinaryUtils.php000064400000003316150251436620007531 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; /** * Provides binary math utilities */ class BinaryUtils { /** * Applies the RFC 4122 variant field to the 16-bit clock sequence * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant * * @param int $clockSeq The 16-bit clock sequence value before the RFC 4122 * variant is applied * * @return int The 16-bit clock sequence multiplexed with the UUID variant * * @psalm-pure */ public static function applyVariant(int $clockSeq): int { $clockSeq = $clockSeq & 0x3fff; $clockSeq |= 0x8000; return $clockSeq; } /** * Applies the RFC 4122 version number to the 16-bit `time_hi_and_version` field * * @link http://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version * * @param int $timeHi The value of the 16-bit `time_hi_and_version` field * before the RFC 4122 version is applied * @param int $version The RFC 4122 version to apply to the `time_hi` field * * @return int The 16-bit time_hi field of the timestamp multiplexed with * the UUID version number * * @psalm-pure */ public static function applyVersion(int $timeHi, int $version): int { $timeHi = $timeHi & 0x0fff; $timeHi |= $version << 12; return $timeHi; } } Validator/ValidatorInterface.php000064400000001760150251436620012760 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Validator; /** * A validator validates a string as a proper UUID * * @psalm-immutable */ interface ValidatorInterface { /** * Returns the regular expression pattern used by this validator * * @return string The regular expression pattern this validator uses * * @psalm-return non-empty-string */ public function getPattern(): string; /** * Returns true if the provided string represents a UUID * * @param string $uuid The string to validate as a UUID * * @return bool True if the string is a valid UUID, false otherwise */ public function validate(string $uuid): bool; } Validator/GenericValidator.php000064400000002552150251436620012434 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Validator; use Ramsey\Uuid\Uuid; use function preg_match; use function str_replace; /** * GenericValidator validates strings as UUIDs of any variant * * @psalm-immutable */ final class GenericValidator implements ValidatorInterface { /** * Regular expression pattern for matching a UUID of any variant. */ private const VALID_PATTERN = '\A[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\z'; /** * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function getPattern(): string { return self::VALID_PATTERN; } public function validate(string $uuid): bool { $uuid = str_replace(['urn:', 'uuid:', 'URN:', 'UUID:', '{', '}'], '', $uuid); return $uuid === Uuid::NIL || preg_match('/' . self::VALID_PATTERN . '/Dms', $uuid); } } Lazy/LazyUuidFromString.php000064400000042654150251436620011774 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Lazy; use DateTimeInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Fields\FieldsInterface; use Ramsey\Uuid\Rfc4122\UuidV1; use Ramsey\Uuid\Rfc4122\UuidV6; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; use ValueError; use function assert; use function bin2hex; use function hex2bin; use function sprintf; use function str_replace; use function substr; /** * Lazy version of a UUID: its format has not been determined yet, so it is mostly only usable for string/bytes * conversion. This object optimizes instantiation, serialization and string conversion time, at the cost of * increased overhead for more advanced UUID operations. * * @internal this type is used internally for performance reasons, and is not supposed to be directly referenced * in consumer libraries. * * @psalm-immutable * * Note: the {@see FieldsInterface} does not declare methods that deprecated API * relies upon: the API has been ported from the {@see \Ramsey\Uuid\Uuid} definition, * and is deprecated anyway. * Note: the deprecated API from {@see \Ramsey\Uuid\Uuid} is in use here (on purpose): it will be removed * once the deprecated API is gone from this class too. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod */ final class LazyUuidFromString implements UuidInterface { public const VALID_REGEX = '/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/ms'; private ?UuidInterface $unwrapped = null; /** * @psalm-param non-empty-string $uuid */ public function __construct(private string $uuid) { } /** @psalm-pure */ public static function fromBytes(string $bytes): self { $base16Uuid = bin2hex($bytes); return new self( substr($base16Uuid, 0, 8) . '-' . substr($base16Uuid, 8, 4) . '-' . substr($base16Uuid, 12, 4) . '-' . substr($base16Uuid, 16, 4) . '-' . substr($base16Uuid, 20, 12) ); } public function serialize(): string { return $this->uuid; } /** * @return array{string: string} * * @psalm-return array{string: non-empty-string} */ public function __serialize(): array { return ['string' => $this->uuid]; } /** * {@inheritDoc} * * @param string $data * * @psalm-param non-empty-string $data */ public function unserialize(string $data): void { $this->uuid = $data; } /** * @param array{string?: string} $data * * @psalm-param array{string?: non-empty-string} $data * @psalm-suppress UnusedMethodCall */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['string'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['string']); } /** @psalm-suppress DeprecatedMethod */ public function getNumberConverter(): NumberConverterInterface { return ($this->unwrapped ?? $this->unwrap()) ->getNumberConverter(); } /** * {@inheritDoc} * * @psalm-suppress DeprecatedMethod */ public function getFieldsHex(): array { return ($this->unwrapped ?? $this->unwrap()) ->getFieldsHex(); } /** @psalm-suppress DeprecatedMethod */ public function getClockSeqHiAndReservedHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getClockSeqHiAndReservedHex(); } /** @psalm-suppress DeprecatedMethod */ public function getClockSeqLowHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getClockSeqLowHex(); } /** @psalm-suppress DeprecatedMethod */ public function getClockSequenceHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getClockSequenceHex(); } /** @psalm-suppress DeprecatedMethod */ public function getDateTime(): DateTimeInterface { return ($this->unwrapped ?? $this->unwrap()) ->getDateTime(); } /** @psalm-suppress DeprecatedMethod */ public function getLeastSignificantBitsHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getLeastSignificantBitsHex(); } /** @psalm-suppress DeprecatedMethod */ public function getMostSignificantBitsHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getMostSignificantBitsHex(); } /** @psalm-suppress DeprecatedMethod */ public function getNodeHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getNodeHex(); } /** @psalm-suppress DeprecatedMethod */ public function getTimeHiAndVersionHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getTimeHiAndVersionHex(); } /** @psalm-suppress DeprecatedMethod */ public function getTimeLowHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getTimeLowHex(); } /** @psalm-suppress DeprecatedMethod */ public function getTimeMidHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getTimeMidHex(); } /** @psalm-suppress DeprecatedMethod */ public function getTimestampHex(): string { return ($this->unwrapped ?? $this->unwrap()) ->getTimestampHex(); } /** @psalm-suppress DeprecatedMethod */ public function getUrn(): string { return ($this->unwrapped ?? $this->unwrap()) ->getUrn(); } /** @psalm-suppress DeprecatedMethod */ public function getVariant(): ?int { return ($this->unwrapped ?? $this->unwrap()) ->getVariant(); } /** @psalm-suppress DeprecatedMethod */ public function getVersion(): ?int { return ($this->unwrapped ?? $this->unwrap()) ->getVersion(); } public function compareTo(UuidInterface $other): int { return ($this->unwrapped ?? $this->unwrap()) ->compareTo($other); } public function equals(?object $other): bool { if (! $other instanceof UuidInterface) { return false; } return $this->uuid === $other->toString(); } /** * {@inheritDoc} * * @psalm-suppress MoreSpecificReturnType * @psalm-suppress LessSpecificReturnStatement we know that {@see self::$uuid} is a non-empty string, so * we know that {@see hex2bin} will retrieve a non-empty string too. */ public function getBytes(): string { /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ return (string) hex2bin(str_replace('-', '', $this->uuid)); } public function getFields(): FieldsInterface { return ($this->unwrapped ?? $this->unwrap()) ->getFields(); } public function getHex(): Hexadecimal { return ($this->unwrapped ?? $this->unwrap()) ->getHex(); } public function getInteger(): IntegerObject { return ($this->unwrapped ?? $this->unwrap()) ->getInteger(); } public function toString(): string { return $this->uuid; } public function __toString(): string { return $this->uuid; } public function jsonSerialize(): string { return $this->uuid; } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getClockSeqHiAndReserved()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getClockSeqHiAndReserved(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getClockSeqHiAndReserved() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getClockSeqLow()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getClockSeqLow(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getClockSeqLow() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getClockSeq()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getClockSequence(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getClockSeq() ->toString() ); } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getLeastSignificantBits(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex(substr($instance->getHex()->toString(), 16)); } /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getMostSignificantBits(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex(substr($instance->getHex()->toString(), 0, 16)); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getNode()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getNode(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getNode() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getTimeHiAndVersion()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getTimeHiAndVersion(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getTimeHiAndVersion() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getTimeLow()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getTimeLow(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getTimeLow() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getTimeMid()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getTimeMid(): string { $instance = ($this->unwrapped ?? $this->unwrap()); return $instance->getNumberConverter() ->fromHex( $instance->getFields() ->getTimeMid() ->toString() ); } /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a {@see Rfc4122FieldsInterface} * instance, you may call {@see Rfc4122FieldsInterface::getTimestamp()} * and use the arbitrary-precision math library of your choice to * convert it to a string integer. * * @psalm-suppress UndefinedInterfaceMethod * @psalm-suppress DeprecatedMethod * @psalm-suppress MixedArgument * @psalm-suppress MixedMethodCall */ public function getTimestamp(): string { $instance = ($this->unwrapped ?? $this->unwrap()); $fields = $instance->getFields(); if ($fields->getVersion() !== 1) { throw new UnsupportedOperationException('Not a time-based UUID'); } return $instance->getNumberConverter() ->fromHex($fields->getTimestamp()->toString()); } public function toUuidV1(): UuidV1 { $instance = ($this->unwrapped ?? $this->unwrap()); if ($instance instanceof UuidV1) { return $instance; } assert($instance instanceof UuidV6); return $instance->toUuidV1(); } public function toUuidV6(): UuidV6 { $instance = ($this->unwrapped ?? $this->unwrap()); assert($instance instanceof UuidV6); return $instance; } /** * @psalm-suppress ImpureMethodCall the retrieval of the factory is a clear violation of purity here: this is a * known pitfall of the design of this library, where a value object contains * a mutable reference to a factory. We use a fixed factory here, so the violation * will not have real-world effects, as this object is only instantiated with the * default factory settings/features. * @psalm-suppress InaccessibleProperty property {@see $unwrapped} is used as a cache: we don't expose it to the * outside world, so we should be fine here. */ private function unwrap(): UuidInterface { return $this->unwrapped = (new UuidFactory()) ->fromString($this->uuid); } } Rfc4122/UuidV2.php000064400000010310150251436620007415 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Uuid; use function hexdec; /** * DCE Security version, or version 2, UUIDs include local domain identifier, * local ID for the specified domain, and node values that are combined into a * 128-bit unsigned integer * * It is important to note that a version 2 UUID suffers from some loss of * fidelity of the timestamp, due to replacing the time_low field with the * local identifier. When constructing the timestamp value for date * purposes, we replace the local identifier bits with zeros. As a result, * the timestamp can be off by a range of 0 to 429.4967295 seconds (or 7 * minutes, 9 seconds, and 496730 microseconds). * * Astute observers might note this value directly corresponds to 2^32 - 1, * or 0xffffffff. The local identifier is 32-bits, and we have set each of * these bits to 0, so the maximum range of timestamp drift is 0x00000000 * to 0xffffffff (counted in 100-nanosecond intervals). * * @link https://publications.opengroup.org/c311 DCE 1.1: Authentication and Security Services * @link https://publications.opengroup.org/c706 DCE 1.1: Remote Procedure Call * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 DCE 1.1: Auth & Sec, §5.2.1.1 * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1: Auth & Sec, §11.5.1.1 * @link https://pubs.opengroup.org/onlinepubs/9629399/apdxa.htm DCE 1.1: RPC, Appendix A * @link https://github.com/google/uuid Go package for UUIDs (includes DCE implementation) * * @psalm-immutable */ final class UuidV2 extends Uuid implements UuidInterface { use TimeTrait; /** * Creates a version 2 (DCE Security) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_DCE_SECURITY) { throw new InvalidArgumentException( 'Fields used to create a UuidV2 must represent a ' . 'version 2 (DCE Security) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } /** * Returns the local domain used to create this version 2 UUID */ public function getLocalDomain(): int { /** @var Rfc4122FieldsInterface $fields */ $fields = $this->getFields(); return (int) hexdec($fields->getClockSeqLow()->toString()); } /** * Returns the string name of the local domain */ public function getLocalDomainName(): string { return Uuid::DCE_DOMAIN_NAMES[$this->getLocalDomain()]; } /** * Returns the local identifier for the domain used to create this version 2 UUID */ public function getLocalIdentifier(): IntegerObject { /** @var Rfc4122FieldsInterface $fields */ $fields = $this->getFields(); return new IntegerObject( $this->numberConverter->fromHex($fields->getTimeLow()->toString()) ); } } Rfc4122/MaxUuid.php000064400000001076150251436620007664 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Uuid; /** * The max UUID is special form of UUID that is specified to have all 128 bits * set to one * * @psalm-immutable */ final class MaxUuid extends Uuid implements UuidInterface { } Rfc4122/UuidV6.php000064400000001415150251436620007427 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Nonstandard\UuidV6 as NonstandardUuidV6; /** * Reordered time, or version 6, UUIDs include timestamp, clock sequence, and * node values that are combined into a 128-bit unsigned integer * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.6 UUID Version 6 * * @psalm-immutable */ final class UuidV6 extends NonstandardUuidV6 implements UuidInterface { } Rfc4122/UuidV7.php000064400000004073150251436620007433 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Unix Epoch time, or version 7, UUIDs include a timestamp in milliseconds * since the Unix Epoch, along with random bytes * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.7 UUID Version 7 * * @psalm-immutable */ final class UuidV7 extends Uuid implements UuidInterface { use TimeTrait; /** * Creates a version 7 (Unix Epoch time) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_UNIX_TIME) { throw new InvalidArgumentException( 'Fields used to create a UuidV7 must represent a ' . 'version 7 (Unix Epoch time) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/MaxTrait.php000064400000002023150251436620010032 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; /** * Provides common functionality for max UUIDs * * The max UUID is special form of UUID that is specified to have all 128 bits * set to one. It is the inverse of the nil UUID. * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.10 Max UUID * * @psalm-immutable */ trait MaxTrait { /** * Returns the bytes that comprise the fields */ abstract public function getBytes(): string; /** * Returns true if the byte string represents a max UUID */ public function isMax(): bool { return $this->getBytes() === "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"; } } Rfc4122/UuidV1.php000064400000003724150251436620007427 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Gregorian time, or version 1, UUIDs include timestamp, clock sequence, and node * values that are combined into a 128-bit unsigned integer * * @psalm-immutable */ final class UuidV1 extends Uuid implements UuidInterface { use TimeTrait; /** * Creates a version 1 (Gregorian time) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_TIME) { throw new InvalidArgumentException( 'Fields used to create a UuidV1 must represent a ' . 'version 1 (time-based) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/UuidV3.php000064400000003724150251436620007431 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Version 3 UUIDs are named-based, using combination of a namespace and name * that are hashed into a 128-bit unsigned integer using MD5 * * @psalm-immutable */ final class UuidV3 extends Uuid implements UuidInterface { /** * Creates a version 3 (name-based, MD5-hashed) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_HASH_MD5) { throw new InvalidArgumentException( 'Fields used to create a UuidV3 must represent a ' . 'version 3 (name-based, MD5-hashed) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/UuidBuilder.php000064400000010733150251436620010525 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\UnixTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Rfc4122\UuidInterface as Rfc4122UuidInterface; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Throwable; /** * UuidBuilder builds instances of RFC 4122 UUIDs * * @psalm-immutable */ class UuidBuilder implements UuidBuilderInterface { private TimeConverterInterface $unixTimeConverter; /** * Constructs the DefaultUuidBuilder * * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the Uuid * @param TimeConverterInterface $timeConverter The time converter to use * for converting Gregorian time extracted from version 1, 2, and 6 * UUIDs to Unix timestamps * @param TimeConverterInterface|null $unixTimeConverter The time converter * to use for converter Unix Epoch time extracted from version 7 UUIDs * to Unix timestamps */ public function __construct( private NumberConverterInterface $numberConverter, private TimeConverterInterface $timeConverter, ?TimeConverterInterface $unixTimeConverter = null ) { $this->unixTimeConverter = $unixTimeConverter ?? new UnixTimeConverter(new BrickMathCalculator()); } /** * Builds and returns a Uuid * * @param CodecInterface $codec The codec to use for building this Uuid instance * @param string $bytes The byte string from which to construct a UUID * * @return Rfc4122UuidInterface UuidBuilder returns instances of Rfc4122UuidInterface * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface { try { /** @var Fields $fields */ $fields = $this->buildFields($bytes); if ($fields->isNil()) { return new NilUuid($fields, $this->numberConverter, $codec, $this->timeConverter); } if ($fields->isMax()) { return new MaxUuid($fields, $this->numberConverter, $codec, $this->timeConverter); } switch ($fields->getVersion()) { case Uuid::UUID_TYPE_TIME: return new UuidV1($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_DCE_SECURITY: return new UuidV2($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_HASH_MD5: return new UuidV3($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_RANDOM: return new UuidV4($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_HASH_SHA1: return new UuidV5($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_REORDERED_TIME: return new UuidV6($fields, $this->numberConverter, $codec, $this->timeConverter); case Uuid::UUID_TYPE_UNIX_TIME: return new UuidV7($fields, $this->numberConverter, $codec, $this->unixTimeConverter); case Uuid::UUID_TYPE_CUSTOM: return new UuidV8($fields, $this->numberConverter, $codec, $this->timeConverter); } throw new UnsupportedOperationException( 'The UUID version in the given fields is not supported ' . 'by this UUID builder' ); } catch (Throwable $e) { throw new UnableToBuildUuidException($e->getMessage(), (int) $e->getCode(), $e); } } /** * Proxy method to allow injecting a mock, for testing */ protected function buildFields(string $bytes): FieldsInterface { return new Fields($bytes); } } Rfc4122/UuidV5.php000064400000003731150251436620007431 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Version 5 UUIDs are named-based, using combination of a namespace and name * that are hashed into a 128-bit unsigned integer using SHA1 * * @psalm-immutable */ final class UuidV5 extends Uuid implements UuidInterface { /** * Creates a version 5 (name-based, SHA1-hashed) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_HASH_SHA1) { throw new InvalidArgumentException( 'Fields used to create a UuidV5 must represent a ' . 'version 5 (named-based, SHA1-hashed) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/TimeTrait.php000064400000002615150251436620010212 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use DateTimeImmutable; use DateTimeInterface; use Ramsey\Uuid\Exception\DateTimeException; use Throwable; use function str_pad; use const STR_PAD_LEFT; /** * Provides common functionality for getting the time from a time-based UUID * * @psalm-immutable */ trait TimeTrait { /** * Returns a DateTimeInterface object representing the timestamp associated * with the UUID * * @return DateTimeImmutable A PHP DateTimeImmutable instance representing * the timestamp of a time-based UUID */ public function getDateTime(): DateTimeInterface { $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); try { return new DateTimeImmutable( '@' . $time->getSeconds()->toString() . '.' . str_pad($time->getMicroseconds()->toString(), 6, '0', STR_PAD_LEFT) ); } catch (Throwable $e) { throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); } } } Rfc4122/Validator.php000064400000002627150251436620010240 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Validator\ValidatorInterface; use function preg_match; use function str_replace; /** * Rfc4122\Validator validates strings as UUIDs of the RFC 4122 variant * * @psalm-immutable */ final class Validator implements ValidatorInterface { private const VALID_PATTERN = '\A[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-' . '[1-8][0-9A-Fa-f]{3}-[ABab89][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}\z'; /** * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function getPattern(): string { return self::VALID_PATTERN; } public function validate(string $uuid): bool { $uuid = str_replace(['urn:', 'uuid:', 'URN:', 'UUID:', '{', '}'], '', $uuid); $uuid = strtolower($uuid); return $uuid === Uuid::NIL || $uuid === Uuid::MAX || preg_match('/' . self::VALID_PATTERN . '/Dms', $uuid); } } Rfc4122/VersionTrait.php000064400000002757150251436620010750 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Uuid; /** * Provides common functionality for handling the version, as defined by RFC 4122 * * @psalm-immutable */ trait VersionTrait { /** * Returns the version */ abstract public function getVersion(): ?int; /** * Returns true if these fields represent a max UUID */ abstract public function isMax(): bool; /** * Returns true if these fields represent a nil UUID */ abstract public function isNil(): bool; /** * Returns true if the version matches one of those defined by RFC 4122 * * @return bool True if the UUID version is valid, false otherwise */ private function isCorrectVersion(): bool { if ($this->isNil() || $this->isMax()) { return true; } return match ($this->getVersion()) { Uuid::UUID_TYPE_TIME, Uuid::UUID_TYPE_DCE_SECURITY, Uuid::UUID_TYPE_HASH_MD5, Uuid::UUID_TYPE_RANDOM, Uuid::UUID_TYPE_HASH_SHA1, Uuid::UUID_TYPE_REORDERED_TIME, Uuid::UUID_TYPE_UNIX_TIME, Uuid::UUID_TYPE_CUSTOM => true, default => false, }; } } Rfc4122/NilTrait.php000064400000001703150251436620010033 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; /** * Provides common functionality for nil UUIDs * * The nil UUID is special form of UUID that is specified to have all 128 bits * set to zero. * * @link https://tools.ietf.org/html/rfc4122#section-4.1.7 RFC 4122, § 4.1.7: Nil UUID * * @psalm-immutable */ trait NilTrait { /** * Returns the bytes that comprise the fields */ abstract public function getBytes(): string; /** * Returns true if the byte string represents a nil UUID */ public function isNil(): bool { return $this->getBytes() === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; } } Rfc4122/UuidV8.php000064400000004445150251436620007437 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Version 8, Custom UUIDs provide an RFC 4122 compatible format for * experimental or vendor-specific uses * * The only requirement for version 8 UUIDs is that the version and variant bits * must be set. Otherwise, implementations are free to set the other bits * according to their needs. As a result, the uniqueness of version 8 UUIDs is * implementation-specific and should not be assumed. * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.8 UUID Version 8 * * @psalm-immutable */ final class UuidV8 extends Uuid implements UuidInterface { /** * Creates a version 8 (custom) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_CUSTOM) { throw new InvalidArgumentException( 'Fields used to create a UuidV8 must represent a ' . 'version 8 (custom) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/FieldsInterface.php000064400000007443150251436620011343 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Fields\FieldsInterface as BaseFieldsInterface; use Ramsey\Uuid\Type\Hexadecimal; /** * RFC 4122 defines fields for a specific variant of UUID * * The fields of an RFC 4122 variant UUID are: * * * **time_low**: The low field of the timestamp, an unsigned 32-bit integer * * **time_mid**: The middle field of the timestamp, an unsigned 16-bit integer * * **time_hi_and_version**: The high field of the timestamp multiplexed with * the version number, an unsigned 16-bit integer * * **clock_seq_hi_and_reserved**: The high field of the clock sequence * multiplexed with the variant, an unsigned 8-bit integer * * **clock_seq_low**: The low field of the clock sequence, an unsigned * 8-bit integer * * **node**: The spatially unique node identifier, an unsigned 48-bit * integer * * @link http://tools.ietf.org/html/rfc4122#section-4.1 RFC 4122, § 4.1: Format * * @psalm-immutable */ interface FieldsInterface extends BaseFieldsInterface { /** * Returns the full 16-bit clock sequence, with the variant bits (two most * significant bits) masked out */ public function getClockSeq(): Hexadecimal; /** * Returns the high field of the clock sequence multiplexed with the variant */ public function getClockSeqHiAndReserved(): Hexadecimal; /** * Returns the low field of the clock sequence */ public function getClockSeqLow(): Hexadecimal; /** * Returns the node field */ public function getNode(): Hexadecimal; /** * Returns the high field of the timestamp multiplexed with the version */ public function getTimeHiAndVersion(): Hexadecimal; /** * Returns the low field of the timestamp */ public function getTimeLow(): Hexadecimal; /** * Returns the middle field of the timestamp */ public function getTimeMid(): Hexadecimal; /** * Returns the full 60-bit timestamp, without the version */ public function getTimestamp(): Hexadecimal; /** * Returns the variant * * The variant number describes the layout of the UUID. The variant * number has the following meaning: * * - 0 - Reserved for NCS backward compatibility * - 2 - The RFC 4122 variant * - 6 - Reserved, Microsoft Corporation backward compatibility * - 7 - Reserved for future definition * * For RFC 4122 variant UUIDs, this value should always be the integer `2`. * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant */ public function getVariant(): int; /** * Returns the version * * The version number describes how the UUID was generated and has the * following meaning: * * 1. Gregorian time UUID * 2. DCE security UUID * 3. Name-based UUID hashed with MD5 * 4. Randomly generated UUID * 5. Name-based UUID hashed with SHA-1 * 6. Reordered time UUID * 7. Unix Epoch time UUID * * This returns `null` if the UUID is not an RFC 4122 variant, since version * is only meaningful for this variant. * * @link http://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public function getVersion(): ?int; /** * Returns true if these fields represent a nil UUID * * The nil UUID is special form of UUID that is specified to have all 128 * bits set to zero. */ public function isNil(): bool; } Rfc4122/Fields.php000064400000013512150251436620007514 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Fields\SerializableFieldsTrait; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Uuid; use function bin2hex; use function dechex; use function hexdec; use function sprintf; use function str_pad; use function strlen; use function substr; use function unpack; use const STR_PAD_LEFT; /** * RFC 4122 variant UUIDs are comprised of a set of named fields * * Internally, this class represents the fields together as a 16-byte binary * string. * * @psalm-immutable */ final class Fields implements FieldsInterface { use MaxTrait; use NilTrait; use SerializableFieldsTrait; use VariantTrait; use VersionTrait; /** * @param string $bytes A 16-byte binary string representation of a UUID * * @throws InvalidArgumentException if the byte string is not exactly 16 bytes * @throws InvalidArgumentException if the byte string does not represent an RFC 4122 UUID * @throws InvalidArgumentException if the byte string does not contain a valid version */ public function __construct(private string $bytes) { if (strlen($this->bytes) !== 16) { throw new InvalidArgumentException( 'The byte string must be 16 bytes long; ' . 'received ' . strlen($this->bytes) . ' bytes' ); } if (!$this->isCorrectVariant()) { throw new InvalidArgumentException( 'The byte string received does not conform to the RFC 4122 variant' ); } if (!$this->isCorrectVersion()) { throw new InvalidArgumentException( 'The byte string received does not contain a valid RFC 4122 version' ); } } public function getBytes(): string { return $this->bytes; } public function getClockSeq(): Hexadecimal { if ($this->isMax()) { $clockSeq = 0xffff; } elseif ($this->isNil()) { $clockSeq = 0x0000; } else { $clockSeq = hexdec(bin2hex(substr($this->bytes, 8, 2))) & 0x3fff; } return new Hexadecimal(str_pad(dechex($clockSeq), 4, '0', STR_PAD_LEFT)); } public function getClockSeqHiAndReserved(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 8, 1))); } public function getClockSeqLow(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 9, 1))); } public function getNode(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 10))); } public function getTimeHiAndVersion(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 6, 2))); } public function getTimeLow(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 0, 4))); } public function getTimeMid(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 4, 2))); } /** * Returns the full 60-bit timestamp, without the version * * For version 2 UUIDs, the time_low field is the local identifier and * should not be returned as part of the time. For this reason, we set the * bottom 32 bits of the timestamp to 0's. As a result, there is some loss * of fidelity of the timestamp, for version 2 UUIDs. The timestamp can be * off by a range of 0 to 429.4967295 seconds (or 7 minutes, 9 seconds, and * 496730 microseconds). * * For version 6 UUIDs, the timestamp order is reversed from the typical RFC * 4122 order (the time bits are in the correct bit order, so that it is * monotonically increasing). In returning the timestamp value, we put the * bits in the order: time_low + time_mid + time_hi. */ public function getTimestamp(): Hexadecimal { $timestamp = match ($this->getVersion()) { Uuid::UUID_TYPE_DCE_SECURITY => sprintf( '%03x%04s%08s', hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, $this->getTimeMid()->toString(), '' ), Uuid::UUID_TYPE_REORDERED_TIME => sprintf( '%08s%04s%03x', $this->getTimeLow()->toString(), $this->getTimeMid()->toString(), hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff ), // The Unix timestamp in version 7 UUIDs is a 48-bit number, // but for consistency, we will return a 60-bit number, padded // to the left with zeros. Uuid::UUID_TYPE_UNIX_TIME => sprintf( '%011s%04s', $this->getTimeLow()->toString(), $this->getTimeMid()->toString(), ), default => sprintf( '%03x%04s%08s', hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, $this->getTimeMid()->toString(), $this->getTimeLow()->toString() ), }; return new Hexadecimal($timestamp); } public function getVersion(): ?int { if ($this->isNil() || $this->isMax()) { return null; } /** @var int[] $parts */ $parts = unpack('n*', $this->bytes); return $parts[4] >> 12; } private function isCorrectVariant(): bool { if ($this->isNil() || $this->isMax()) { return true; } return $this->getVariant() === Uuid::RFC_4122; } } Rfc4122/UuidV4.php000064400000003604150251436620007427 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; /** * Random, or version 4, UUIDs are randomly or pseudo-randomly generated 128-bit * integers * * @psalm-immutable */ final class UuidV4 extends Uuid implements UuidInterface { /** * Creates a version 4 (random) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_RANDOM) { throw new InvalidArgumentException( 'Fields used to create a UuidV4 must represent a ' . 'version 4 (random) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Rfc4122/NilUuid.php000064400000001077150251436620007662 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Uuid; /** * The nil UUID is special form of UUID that is specified to have all 128 bits * set to zero * * @psalm-immutable */ final class NilUuid extends Uuid implements UuidInterface { } Rfc4122/UuidInterface.php000064400000001257150251436620011040 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\UuidInterface as BaseUuidInterface; /** * Also known as a Leach-Salz variant UUID, an RFC 4122 variant UUID is a * universally unique identifier defined by RFC 4122 * * @link https://tools.ietf.org/html/rfc4122 RFC 4122 * * @psalm-immutable */ interface UuidInterface extends BaseUuidInterface { } Rfc4122/VariantTrait.php000064400000005170150251436620010717 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; use Ramsey\Uuid\Exception\InvalidBytesException; use Ramsey\Uuid\Uuid; use function decbin; use function str_pad; use function str_starts_with; use function strlen; use function substr; use function unpack; use const STR_PAD_LEFT; /** * Provides common functionality for handling the variant, as defined by RFC 4122 * * @psalm-immutable */ trait VariantTrait { /** * Returns the bytes that comprise the fields */ abstract public function getBytes(): string; /** * Returns the variant identifier, according to RFC 4122, for the given bytes * * The following values may be returned: * * - `0` -- Reserved, NCS backward compatibility. * - `2` -- The variant specified in RFC 4122. * - `6` -- Reserved, Microsoft Corporation backward compatibility. * - `7` -- Reserved for future definition. * * @link https://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant * * @return int The variant identifier, according to RFC 4122 */ public function getVariant(): int { if (strlen($this->getBytes()) !== 16) { throw new InvalidBytesException('Invalid number of bytes'); } if ($this->isMax() || $this->isNil()) { // RFC 4122 defines these special types of UUID, so we will consider // them as belonging to the RFC 4122 variant. return Uuid::RFC_4122; } /** @var int[] $parts */ $parts = unpack('n*', $this->getBytes()); // $parts[5] is a 16-bit, unsigned integer containing the variant bits // of the UUID. We convert this integer into a string containing a // binary representation, padded to 16 characters. We analyze the first // three characters (three most-significant bits) to determine the // variant. $binary = str_pad( decbin($parts[5]), 16, '0', STR_PAD_LEFT ); $msb = substr($binary, 0, 3); if ($msb === '111') { return Uuid::RESERVED_FUTURE; } elseif ($msb === '110') { return Uuid::RESERVED_MICROSOFT; } elseif (str_starts_with($msb, '10')) { return Uuid::RFC_4122; } return Uuid::RESERVED_NCS; } } FeatureSet.php000064400000030303150251436620007327 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use Ramsey\Uuid\Builder\FallbackBuilder; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Codec\GuidStringCodec; use Ramsey\Uuid\Codec\StringCodec; use Ramsey\Uuid\Converter\Number\GenericNumberConverter; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Converter\Time\PhpTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Generator\DceSecurityGenerator; use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; use Ramsey\Uuid\Generator\NameGeneratorFactory; use Ramsey\Uuid\Generator\NameGeneratorInterface; use Ramsey\Uuid\Generator\PeclUuidNameGenerator; use Ramsey\Uuid\Generator\PeclUuidRandomGenerator; use Ramsey\Uuid\Generator\PeclUuidTimeGenerator; use Ramsey\Uuid\Generator\RandomGeneratorFactory; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorFactory; use Ramsey\Uuid\Generator\TimeGeneratorInterface; use Ramsey\Uuid\Generator\UnixTimeGenerator; use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Nonstandard\UuidBuilder as NonstandardUuidBuilder; use Ramsey\Uuid\Provider\Dce\SystemDceSecurityProvider; use Ramsey\Uuid\Provider\DceSecurityProviderInterface; use Ramsey\Uuid\Provider\Node\FallbackNodeProvider; use Ramsey\Uuid\Provider\Node\RandomNodeProvider; use Ramsey\Uuid\Provider\Node\SystemNodeProvider; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Provider\Time\SystemTimeProvider; use Ramsey\Uuid\Provider\TimeProviderInterface; use Ramsey\Uuid\Rfc4122\UuidBuilder as Rfc4122UuidBuilder; use Ramsey\Uuid\Validator\GenericValidator; use Ramsey\Uuid\Validator\ValidatorInterface; use const PHP_INT_SIZE; /** * FeatureSet detects and exposes available features in the current environment * * A feature set is used by UuidFactory to determine the available features and * capabilities of the environment. */ class FeatureSet { private ?TimeProviderInterface $timeProvider = null; private CalculatorInterface $calculator; private CodecInterface $codec; private DceSecurityGeneratorInterface $dceSecurityGenerator; private NameGeneratorInterface $nameGenerator; private NodeProviderInterface $nodeProvider; private NumberConverterInterface $numberConverter; private RandomGeneratorInterface $randomGenerator; private TimeConverterInterface $timeConverter; private TimeGeneratorInterface $timeGenerator; private TimeGeneratorInterface $unixTimeGenerator; private UuidBuilderInterface $builder; private ValidatorInterface $validator; /** * @param bool $useGuids True build UUIDs using the GuidStringCodec * @param bool $force32Bit True to force the use of 32-bit functionality * (primarily for testing purposes) * @param bool $forceNoBigNumber (obsolete) * @param bool $ignoreSystemNode True to disable attempts to check for the * system node ID (primarily for testing purposes) * @param bool $enablePecl True to enable the use of the PeclUuidTimeGenerator * to generate version 1 UUIDs */ public function __construct( bool $useGuids = false, private bool $force32Bit = false, bool $forceNoBigNumber = false, private bool $ignoreSystemNode = false, private bool $enablePecl = false ) { $this->randomGenerator = $this->buildRandomGenerator(); $this->setCalculator(new BrickMathCalculator()); $this->builder = $this->buildUuidBuilder($useGuids); $this->codec = $this->buildCodec($useGuids); $this->nodeProvider = $this->buildNodeProvider(); $this->nameGenerator = $this->buildNameGenerator(); $this->setTimeProvider(new SystemTimeProvider()); $this->setDceSecurityProvider(new SystemDceSecurityProvider()); $this->validator = new GenericValidator(); assert($this->timeProvider !== null); $this->unixTimeGenerator = $this->buildUnixTimeGenerator(); } /** * Returns the builder configured for this environment */ public function getBuilder(): UuidBuilderInterface { return $this->builder; } /** * Returns the calculator configured for this environment */ public function getCalculator(): CalculatorInterface { return $this->calculator; } /** * Returns the codec configured for this environment */ public function getCodec(): CodecInterface { return $this->codec; } /** * Returns the DCE Security generator configured for this environment */ public function getDceSecurityGenerator(): DceSecurityGeneratorInterface { return $this->dceSecurityGenerator; } /** * Returns the name generator configured for this environment */ public function getNameGenerator(): NameGeneratorInterface { return $this->nameGenerator; } /** * Returns the node provider configured for this environment */ public function getNodeProvider(): NodeProviderInterface { return $this->nodeProvider; } /** * Returns the number converter configured for this environment */ public function getNumberConverter(): NumberConverterInterface { return $this->numberConverter; } /** * Returns the random generator configured for this environment */ public function getRandomGenerator(): RandomGeneratorInterface { return $this->randomGenerator; } /** * Returns the time converter configured for this environment */ public function getTimeConverter(): TimeConverterInterface { return $this->timeConverter; } /** * Returns the time generator configured for this environment */ public function getTimeGenerator(): TimeGeneratorInterface { return $this->timeGenerator; } /** * Returns the Unix Epoch time generator configured for this environment */ public function getUnixTimeGenerator(): TimeGeneratorInterface { return $this->unixTimeGenerator; } /** * Returns the validator configured for this environment */ public function getValidator(): ValidatorInterface { return $this->validator; } /** * Sets the calculator to use in this environment */ public function setCalculator(CalculatorInterface $calculator): void { $this->calculator = $calculator; $this->numberConverter = $this->buildNumberConverter($calculator); $this->timeConverter = $this->buildTimeConverter($calculator); /** @psalm-suppress RedundantPropertyInitializationCheck */ if (isset($this->timeProvider)) { $this->timeGenerator = $this->buildTimeGenerator($this->timeProvider); } } /** * Sets the DCE Security provider to use in this environment */ public function setDceSecurityProvider(DceSecurityProviderInterface $dceSecurityProvider): void { $this->dceSecurityGenerator = $this->buildDceSecurityGenerator($dceSecurityProvider); } /** * Sets the node provider to use in this environment */ public function setNodeProvider(NodeProviderInterface $nodeProvider): void { $this->nodeProvider = $nodeProvider; if (isset($this->timeProvider)) { $this->timeGenerator = $this->buildTimeGenerator($this->timeProvider); } } /** * Sets the time provider to use in this environment */ public function setTimeProvider(TimeProviderInterface $timeProvider): void { $this->timeProvider = $timeProvider; $this->timeGenerator = $this->buildTimeGenerator($timeProvider); } /** * Set the validator to use in this environment */ public function setValidator(ValidatorInterface $validator): void { $this->validator = $validator; } /** * Returns a codec configured for this environment * * @param bool $useGuids Whether to build UUIDs using the GuidStringCodec */ private function buildCodec(bool $useGuids = false): CodecInterface { if ($useGuids) { return new GuidStringCodec($this->builder); } return new StringCodec($this->builder); } /** * Returns a DCE Security generator configured for this environment */ private function buildDceSecurityGenerator( DceSecurityProviderInterface $dceSecurityProvider ): DceSecurityGeneratorInterface { return new DceSecurityGenerator( $this->numberConverter, $this->timeGenerator, $dceSecurityProvider ); } /** * Returns a node provider configured for this environment */ private function buildNodeProvider(): NodeProviderInterface { if ($this->ignoreSystemNode) { return new RandomNodeProvider(); } return new FallbackNodeProvider([ new SystemNodeProvider(), new RandomNodeProvider(), ]); } /** * Returns a number converter configured for this environment */ private function buildNumberConverter(CalculatorInterface $calculator): NumberConverterInterface { return new GenericNumberConverter($calculator); } /** * Returns a random generator configured for this environment */ private function buildRandomGenerator(): RandomGeneratorInterface { if ($this->enablePecl) { return new PeclUuidRandomGenerator(); } return (new RandomGeneratorFactory())->getGenerator(); } /** * Returns a time generator configured for this environment * * @param TimeProviderInterface $timeProvider The time provider to use with * the time generator */ private function buildTimeGenerator(TimeProviderInterface $timeProvider): TimeGeneratorInterface { if ($this->enablePecl) { return new PeclUuidTimeGenerator(); } return (new TimeGeneratorFactory( $this->nodeProvider, $this->timeConverter, $timeProvider ))->getGenerator(); } /** * Returns a Unix Epoch time generator configured for this environment */ private function buildUnixTimeGenerator(): TimeGeneratorInterface { return new UnixTimeGenerator($this->randomGenerator); } /** * Returns a name generator configured for this environment */ private function buildNameGenerator(): NameGeneratorInterface { if ($this->enablePecl) { return new PeclUuidNameGenerator(); } return (new NameGeneratorFactory())->getGenerator(); } /** * Returns a time converter configured for this environment */ private function buildTimeConverter(CalculatorInterface $calculator): TimeConverterInterface { $genericConverter = new GenericTimeConverter($calculator); if ($this->is64BitSystem()) { return new PhpTimeConverter($calculator, $genericConverter); } return $genericConverter; } /** * Returns a UUID builder configured for this environment * * @param bool $useGuids Whether to build UUIDs using the GuidStringCodec */ private function buildUuidBuilder(bool $useGuids = false): UuidBuilderInterface { if ($useGuids) { return new GuidBuilder($this->numberConverter, $this->timeConverter); } return new FallbackBuilder([ new Rfc4122UuidBuilder($this->numberConverter, $this->timeConverter), new NonstandardUuidBuilder($this->numberConverter, $this->timeConverter), ]); } /** * Returns true if the PHP build is 64-bit */ private function is64BitSystem(): bool { return PHP_INT_SIZE === 8 && !$this->force32Bit; } } Type/Time.php000064400000006570150251436620007110 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Type\Integer as IntegerObject; use ValueError; use function json_decode; use function json_encode; use function sprintf; /** * A value object representing a timestamp * * This class exists for type-safety purposes, to ensure that timestamps used * by ramsey/uuid are truly timestamp integers and not some other kind of string * or integer. * * @psalm-immutable */ final class Time implements TypeInterface { private IntegerObject $seconds; private IntegerObject $microseconds; public function __construct( float | int | string | IntegerObject $seconds, float | int | string | IntegerObject $microseconds = 0, ) { $this->seconds = new IntegerObject($seconds); $this->microseconds = new IntegerObject($microseconds); } public function getSeconds(): IntegerObject { return $this->seconds; } public function getMicroseconds(): IntegerObject { return $this->microseconds; } public function toString(): string { return $this->seconds->toString() . '.' . sprintf('%06s', $this->microseconds->toString()); } public function __toString(): string { return $this->toString(); } /** * @return string[] */ public function jsonSerialize(): array { return [ 'seconds' => $this->getSeconds()->toString(), 'microseconds' => $this->getMicroseconds()->toString(), ]; } public function serialize(): string { return (string) json_encode($this); } /** * @return array{seconds: string, microseconds: string} */ public function __serialize(): array { return [ 'seconds' => $this->getSeconds()->toString(), 'microseconds' => $this->getMicroseconds()->toString(), ]; } /** * Constructs the object from a serialized string representation * * @param string $data The serialized string representation of the object * * @psalm-suppress UnusedMethodCall */ public function unserialize(string $data): void { /** @var array{seconds?: int|float|string, microseconds?: int|float|string} $time */ $time = json_decode($data, true); if (!isset($time['seconds']) || !isset($time['microseconds'])) { throw new UnsupportedOperationException( 'Attempted to unserialize an invalid value' ); } $this->__construct($time['seconds'], $time['microseconds']); } /** * @param array{seconds?: string, microseconds?: string} $data */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['seconds']) || !isset($data['microseconds'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->__construct($data['seconds'], $data['microseconds']); } } Type/NumberInterface.php000064400000001173150251436620011255 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; /** * NumberInterface ensures consistency in numeric values returned by ramsey/uuid * * @psalm-immutable */ interface NumberInterface extends TypeInterface { /** * Returns true if this number is less than zero */ public function isNegative(): bool; } Type/Hexadecimal.php000064400000005227150251436620010414 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\InvalidArgumentException; use ValueError; use function preg_match; use function sprintf; use function substr; /** * A value object representing a hexadecimal number * * This class exists for type-safety purposes, to ensure that hexadecimal numbers * returned from ramsey/uuid methods as strings are truly hexadecimal and not some * other kind of string. * * @psalm-immutable */ final class Hexadecimal implements TypeInterface { private string $value; /** * @param self|string $value The hexadecimal value to store */ public function __construct(self | string $value) { $this->value = $value instanceof self ? (string) $value : $this->prepareValue($value); } public function toString(): string { return $this->value; } public function __toString(): string { return $this->toString(); } public function jsonSerialize(): string { return $this->toString(); } public function serialize(): string { return $this->toString(); } /** * @return array{string: string} */ public function __serialize(): array { return ['string' => $this->toString()]; } /** * Constructs the object from a serialized string representation * * @param string $data The serialized string representation of the object * * @psalm-suppress UnusedMethodCall */ public function unserialize(string $data): void { $this->__construct($data); } /** * @param array{string?: string} $data */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['string'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['string']); } private function prepareValue(string $value): string { $value = strtolower($value); if (str_starts_with($value, '0x')) { $value = substr($value, 2); } if (!preg_match('/^[A-Fa-f0-9]+$/', $value)) { throw new InvalidArgumentException( 'Value must be a hexadecimal number' ); } return $value; } } Type/Decimal.php000064400000006053150251436620007544 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\InvalidArgumentException; use ValueError; use function is_numeric; use function sprintf; use function str_starts_with; /** * A value object representing a decimal * * This class exists for type-safety purposes, to ensure that decimals * returned from ramsey/uuid methods as strings are truly decimals and not some * other kind of string. * * To support values as true decimals and not as floats or doubles, we store the * decimals as strings. * * @psalm-immutable */ final class Decimal implements NumberInterface { private string $value; private bool $isNegative = false; public function __construct(float | int | string | self $value) { $value = (string) $value; if (!is_numeric($value)) { throw new InvalidArgumentException( 'Value must be a signed decimal or a string containing only ' . 'digits 0-9 and, optionally, a decimal point or sign (+ or -)' ); } // Remove the leading +-symbol. if (str_starts_with($value, '+')) { $value = substr($value, 1); } // For cases like `-0` or `-0.0000`, convert the value to `0`. if (abs((float) $value) === 0.0) { $value = '0'; } if (str_starts_with($value, '-')) { $this->isNegative = true; } $this->value = $value; } public function isNegative(): bool { return $this->isNegative; } public function toString(): string { return $this->value; } public function __toString(): string { return $this->toString(); } public function jsonSerialize(): string { return $this->toString(); } public function serialize(): string { return $this->toString(); } /** * @return array{string: string} */ public function __serialize(): array { return ['string' => $this->toString()]; } /** * Constructs the object from a serialized string representation * * @param string $data The serialized string representation of the object * * @psalm-suppress UnusedMethodCall */ public function unserialize(string $data): void { $this->__construct($data); } /** * @param array{string?: string} $data * * @psalm-suppress UnusedMethodCall */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['string'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['string']); } } Type/TypeInterface.php000064400000001225150251436620010744 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; use JsonSerializable; use Serializable; /** * TypeInterface ensures consistency in typed values returned by ramsey/uuid * * @psalm-immutable */ interface TypeInterface extends JsonSerializable, Serializable { public function toString(): string; public function __toString(): string; } Type/Integer.php000064400000007443150251436620007607 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\InvalidArgumentException; use ValueError; use function assert; use function is_numeric; use function preg_match; use function sprintf; use function substr; /** * A value object representing an integer * * This class exists for type-safety purposes, to ensure that integers * returned from ramsey/uuid methods as strings are truly integers and not some * other kind of string. * * To support large integers beyond PHP_INT_MAX and PHP_INT_MIN on both 64-bit * and 32-bit systems, we store the integers as strings. * * @psalm-immutable */ final class Integer implements NumberInterface { /** * @psalm-var numeric-string */ private string $value; private bool $isNegative = false; public function __construct(float | int | string | self $value) { $this->value = $value instanceof self ? (string) $value : $this->prepareValue($value); } public function isNegative(): bool { return $this->isNegative; } /** * @psalm-return numeric-string */ public function toString(): string { return $this->value; } /** * @psalm-return numeric-string */ public function __toString(): string { return $this->toString(); } public function jsonSerialize(): string { return $this->toString(); } public function serialize(): string { return $this->toString(); } /** * @return array{string: string} */ public function __serialize(): array { return ['string' => $this->toString()]; } /** * Constructs the object from a serialized string representation * * @param string $data The serialized string representation of the object * * @psalm-suppress UnusedMethodCall */ public function unserialize(string $data): void { $this->__construct($data); } /** * @param array{string?: string} $data */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['string'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['string']); } /** * @return numeric-string */ private function prepareValue(float | int | string $value): string { $value = (string) $value; $sign = '+'; // If the value contains a sign, remove it for digit pattern check. if (str_starts_with($value, '-') || str_starts_with($value, '+')) { $sign = substr($value, 0, 1); $value = substr($value, 1); } if (!preg_match('/^\d+$/', $value)) { throw new InvalidArgumentException( 'Value must be a signed integer or a string containing only ' . 'digits 0-9 and, optionally, a sign (+ or -)' ); } // Trim any leading zeros. $value = ltrim($value, '0'); // Set to zero if the string is empty after trimming zeros. if ($value === '') { $value = '0'; } // Add the negative sign back to the value. if ($sign === '-' && $value !== '0') { $value = $sign . $value; /** @psalm-suppress InaccessibleProperty */ $this->isNegative = true; } assert(is_numeric($value)); return $value; } } Nonstandard/UuidV6.php000064400000006474150251436620010671 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Nonstandard; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Lazy\LazyUuidFromString; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Rfc4122\TimeTrait; use Ramsey\Uuid\Rfc4122\UuidInterface; use Ramsey\Uuid\Rfc4122\UuidV1; use Ramsey\Uuid\Uuid; /** * Reordered time, or version 6, UUIDs include timestamp, clock sequence, and * node values that are combined into a 128-bit unsigned integer * * @deprecated Use {@see \Ramsey\Uuid\Rfc4122\UuidV6} instead. * * @link https://github.com/uuid6/uuid6-ietf-draft UUID version 6 IETF draft * @link http://gh.peabody.io/uuidv6/ "Version 6" UUIDs * * @psalm-immutable */ class UuidV6 extends Uuid implements UuidInterface { use TimeTrait; /** * Creates a version 6 (reordered time) UUID * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { if ($fields->getVersion() !== Uuid::UUID_TYPE_REORDERED_TIME) { throw new InvalidArgumentException( 'Fields used to create a UuidV6 must represent a ' . 'version 6 (reordered time) UUID' ); } parent::__construct($fields, $numberConverter, $codec, $timeConverter); } /** * Converts this UUID into an instance of a version 1 UUID */ public function toUuidV1(): UuidV1 { $hex = $this->getHex()->toString(); $hex = substr($hex, 7, 5) . substr($hex, 13, 3) . substr($hex, 3, 4) . '1' . substr($hex, 0, 3) . substr($hex, 16); /** @var LazyUuidFromString $uuid */ $uuid = Uuid::fromBytes((string) hex2bin($hex)); return $uuid->toUuidV1(); } /** * Converts a version 1 UUID into an instance of a version 6 UUID */ public static function fromUuidV1(UuidV1 $uuidV1): \Ramsey\Uuid\Rfc4122\UuidV6 { $hex = $uuidV1->getHex()->toString(); $hex = substr($hex, 13, 3) . substr($hex, 8, 4) . substr($hex, 0, 5) . '6' . substr($hex, 5, 3) . substr($hex, 16); /** @var LazyUuidFromString $uuid */ $uuid = Uuid::fromBytes((string) hex2bin($hex)); return $uuid->toUuidV6(); } } Nonstandard/UuidBuilder.php000064400000004276150251436620011762 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Nonstandard; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\UuidInterface; use Throwable; /** * Nonstandard\UuidBuilder builds instances of Nonstandard\Uuid * * @psalm-immutable */ class UuidBuilder implements UuidBuilderInterface { /** * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the Nonstandard\Uuid * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to Unix timestamps */ public function __construct( private NumberConverterInterface $numberConverter, private TimeConverterInterface $timeConverter ) { } /** * Builds and returns a Nonstandard\Uuid * * @param CodecInterface $codec The codec to use for building this instance * @param string $bytes The byte string from which to construct a UUID * * @return Uuid The Nonstandard\UuidBuilder returns an instance of * Nonstandard\Uuid * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface { try { return new Uuid( $this->buildFields($bytes), $this->numberConverter, $codec, $this->timeConverter ); } catch (Throwable $e) { throw new UnableToBuildUuidException($e->getMessage(), (int) $e->getCode(), $e); } } /** * Proxy method to allow injecting a mock, for testing */ protected function buildFields(string $bytes): Fields { return new Fields($bytes); } } Nonstandard/Uuid.php000064400000001673150251436620010451 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Nonstandard; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Uuid as BaseUuid; /** * Nonstandard\Uuid is a UUID that doesn't conform to RFC 4122 * * @psalm-immutable */ final class Uuid extends BaseUuid { public function __construct( Fields $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Nonstandard/Fields.php000064400000006457150251436620010756 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Nonstandard; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Fields\SerializableFieldsTrait; use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\Rfc4122\VariantTrait; use Ramsey\Uuid\Type\Hexadecimal; use function bin2hex; use function dechex; use function hexdec; use function sprintf; use function str_pad; use function strlen; use function substr; use const STR_PAD_LEFT; /** * Nonstandard UUID fields do not conform to the RFC 4122 standard * * Since some systems may create nonstandard UUIDs, this implements the * Rfc4122\FieldsInterface, so that functionality of a nonstandard UUID is not * degraded, in the event these UUIDs are expected to contain RFC 4122 fields. * * Internally, this class represents the fields together as a 16-byte binary * string. * * @psalm-immutable */ final class Fields implements FieldsInterface { use SerializableFieldsTrait; use VariantTrait; /** * @param string $bytes A 16-byte binary string representation of a UUID * * @throws InvalidArgumentException if the byte string is not exactly 16 bytes */ public function __construct(private string $bytes) { if (strlen($this->bytes) !== 16) { throw new InvalidArgumentException( 'The byte string must be 16 bytes long; ' . 'received ' . strlen($this->bytes) . ' bytes' ); } } public function getBytes(): string { return $this->bytes; } public function getClockSeq(): Hexadecimal { $clockSeq = hexdec(bin2hex(substr($this->bytes, 8, 2))) & 0x3fff; return new Hexadecimal(str_pad(dechex($clockSeq), 4, '0', STR_PAD_LEFT)); } public function getClockSeqHiAndReserved(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 8, 1))); } public function getClockSeqLow(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 9, 1))); } public function getNode(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 10))); } public function getTimeHiAndVersion(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 6, 2))); } public function getTimeLow(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 0, 4))); } public function getTimeMid(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 4, 2))); } public function getTimestamp(): Hexadecimal { return new Hexadecimal(sprintf( '%03x%04s%08s', hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, $this->getTimeMid()->toString(), $this->getTimeLow()->toString() )); } public function getVersion(): ?int { return null; } public function isNil(): bool { return false; } public function isMax(): bool { return false; } } Exception/TimeSourceException.php000064400000001104150251436620013151 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that the source of time encountered an error */ class TimeSourceException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/DateTimeException.php000064400000001124150251436620012570 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that the PHP DateTime extension encountered an exception/error */ class DateTimeException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/RandomSourceException.php000064400000001362150251436620013501 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that the source of random data encountered an error * * This exception is used mostly to indicate that random_bytes() or random_int() * threw an exception. However, it may be used for other sources of random data. */ class RandomSourceException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/DceSecurityException.php000064400000001143150251436620013320 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate an exception occurred while dealing with DCE Security * (version 2) UUIDs */ class DceSecurityException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/UnsupportedOperationException.php000064400000001111150251436620015301 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use LogicException as PhpLogicException; /** * Thrown to indicate that the requested operation is not supported */ class UnsupportedOperationException extends PhpLogicException implements UuidExceptionInterface { } Exception/BuilderNotFoundException.php000064400000001104150251436620014135 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that no suitable builder could be found */ class BuilderNotFoundException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/InvalidUuidStringException.php000064400000001267150251436620014510 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; /** * Thrown to indicate that the string received is not a valid UUID * * The InvalidArgumentException that this extends is the ramsey/uuid version * of this exception. It exists in the same namespace as this class. */ class InvalidUuidStringException extends InvalidArgumentException implements UuidExceptionInterface { } Exception/UuidExceptionInterface.php000064400000000666150251436620013635 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use Throwable; interface UuidExceptionInterface extends Throwable { } Exception/InvalidArgumentException.php000064400000001134150251436620014166 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use InvalidArgumentException as PhpInvalidArgumentException; /** * Thrown to indicate that the argument received is not valid */ class InvalidArgumentException extends PhpInvalidArgumentException implements UuidExceptionInterface { } Exception/InvalidBytesException.php000064400000001122150251436620013467 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that the bytes being operated on are invalid in some way */ class InvalidBytesException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/NameException.php000064400000001131150251436620011752 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that an error occurred while attempting to hash a * namespace and name */ class NameException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/NodeException.php000064400000001123150251436620011760 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate that attempting to fetch or create a node ID encountered an error */ class NodeException extends PhpRuntimeException implements UuidExceptionInterface { } Exception/UnableToBuildUuidException.php000064400000001102150251436620014410 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Exception; use RuntimeException as PhpRuntimeException; /** * Thrown to indicate a builder is unable to build a UUID */ class UnableToBuildUuidException extends PhpRuntimeException implements UuidExceptionInterface { } UuidFactoryInterface.php000064400000013746150251436620011353 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use DateTimeInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Validator\ValidatorInterface; /** * UuidFactoryInterface defines common functionality all `UuidFactory` instances * must implement */ interface UuidFactoryInterface { /** * Creates a UUID from a byte string * * @param string $bytes A binary string * * @return UuidInterface A UuidInterface instance created from a binary * string representation * * @psalm-pure */ public function fromBytes(string $bytes): UuidInterface; /** * Creates a UUID from a DateTimeInterface instance * * @param DateTimeInterface $dateTime The date and time * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return UuidInterface A UuidInterface instance that represents a * version 1 UUID created from a DateTimeInterface instance */ public function fromDateTime( DateTimeInterface $dateTime, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface; /** * Creates a UUID from a 128-bit integer string * * @param string $integer String representation of 128-bit integer * * @return UuidInterface A UuidInterface instance created from the string * representation of a 128-bit integer * * @psalm-pure */ public function fromInteger(string $integer): UuidInterface; /** * Creates a UUID from the string standard representation * * @param string $uuid A hexadecimal string * * @return UuidInterface A UuidInterface instance created from a hexadecimal * string representation * * @psalm-pure */ public function fromString(string $uuid): UuidInterface; /** * Returns the validator to use for the factory * * @psalm-mutation-free */ public function getValidator(): ValidatorInterface; /** * Returns a version 1 (Gregorian time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|int|string|null $node A 48-bit number representing the * hardware address; this number may be represented as an integer or a * hexadecimal string * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return UuidInterface A UuidInterface instance that represents a * version 1 UUID */ public function uuid1($node = null, ?int $clockSeq = null): UuidInterface; /** * Returns a version 2 (DCE Security) UUID from a local domain, local * identifier, host ID, clock sequence, and the current time * * @param int $localDomain The local domain to use when generating bytes, * according to DCE Security * @param IntegerObject|null $localIdentifier The local identifier for the * given domain; this may be a UID or GID on POSIX systems, if the local * domain is person or group, or it may be a site-defined identifier * if the local domain is org * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return UuidInterface A UuidInterface instance that represents a * version 2 UUID */ public function uuid2( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface; /** * Returns a version 3 (name-based) UUID based on the MD5 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * @param string $name The name to use for creating a UUID * * @return UuidInterface A UuidInterface instance that represents a * version 3 UUID * * @psalm-pure */ public function uuid3($ns, string $name): UuidInterface; /** * Returns a version 4 (random) UUID * * @return UuidInterface A UuidInterface instance that represents a * version 4 UUID */ public function uuid4(): UuidInterface; /** * Returns a version 5 (name-based) UUID based on the SHA-1 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * @param string $name The name to use for creating a UUID * * @return UuidInterface A UuidInterface instance that represents a * version 5 UUID * * @psalm-pure */ public function uuid5($ns, string $name): UuidInterface; /** * Returns a version 6 (reordered time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return UuidInterface A UuidInterface instance that represents a * version 6 UUID */ public function uuid6(?Hexadecimal $node = null, ?int $clockSeq = null): UuidInterface; } Math/CalculatorInterface.php000064400000007176150251436620012077 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Math; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\NumberInterface; /** * A calculator performs arithmetic operations on numbers * * @psalm-immutable */ interface CalculatorInterface { /** * Returns the sum of all the provided parameters * * @param NumberInterface $augend The first addend (the integer being added to) * @param NumberInterface ...$addends The additional integers to a add to the augend * * @return NumberInterface The sum of all the parameters */ public function add(NumberInterface $augend, NumberInterface ...$addends): NumberInterface; /** * Returns the difference of all the provided parameters * * @param NumberInterface $minuend The integer being subtracted from * @param NumberInterface ...$subtrahends The integers to subtract from the minuend * * @return NumberInterface The difference after subtracting all parameters */ public function subtract(NumberInterface $minuend, NumberInterface ...$subtrahends): NumberInterface; /** * Returns the product of all the provided parameters * * @param NumberInterface $multiplicand The integer to be multiplied * @param NumberInterface ...$multipliers The factors by which to multiply the multiplicand * * @return NumberInterface The product of multiplying all the provided parameters */ public function multiply(NumberInterface $multiplicand, NumberInterface ...$multipliers): NumberInterface; /** * Returns the quotient of the provided parameters divided left-to-right * * @param int $roundingMode The RoundingMode constant to use for this operation * @param int $scale The scale to use for this operation * @param NumberInterface $dividend The integer to be divided * @param NumberInterface ...$divisors The integers to divide $dividend by, in * the order in which the division operations should take place * (left-to-right) * * @return NumberInterface The quotient of dividing the provided parameters left-to-right */ public function divide( int $roundingMode, int $scale, NumberInterface $dividend, NumberInterface ...$divisors ): NumberInterface; /** * Converts a value from an arbitrary base to a base-10 integer value * * @param string $value The value to convert * @param int $base The base to convert from (i.e., 2, 16, 32, etc.) * * @return IntegerObject The base-10 integer value of the converted value */ public function fromBase(string $value, int $base): IntegerObject; /** * Converts a base-10 integer value to an arbitrary base * * @param IntegerObject $value The integer value to convert * @param int $base The base to convert to (i.e., 2, 16, 32, etc.) * * @return string The value represented in the specified base */ public function toBase(IntegerObject $value, int $base): string; /** * Converts an Integer instance to a Hexadecimal instance */ public function toHexadecimal(IntegerObject $value): Hexadecimal; /** * Converts a Hexadecimal instance to an Integer instance */ public function toInteger(Hexadecimal $value): IntegerObject; } Math/BrickMathCalculator.php000064400000010745150251436620012037 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Math; use Brick\Math\BigDecimal; use Brick\Math\BigInteger; use Brick\Math\Exception\MathException; use Brick\Math\RoundingMode as BrickMathRounding; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Type\Decimal; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\NumberInterface; /** * A calculator using the brick/math library for arbitrary-precision arithmetic * * @psalm-immutable */ final class BrickMathCalculator implements CalculatorInterface { private const ROUNDING_MODE_MAP = [ RoundingMode::UNNECESSARY => BrickMathRounding::UNNECESSARY, RoundingMode::UP => BrickMathRounding::UP, RoundingMode::DOWN => BrickMathRounding::DOWN, RoundingMode::CEILING => BrickMathRounding::CEILING, RoundingMode::FLOOR => BrickMathRounding::FLOOR, RoundingMode::HALF_UP => BrickMathRounding::HALF_UP, RoundingMode::HALF_DOWN => BrickMathRounding::HALF_DOWN, RoundingMode::HALF_CEILING => BrickMathRounding::HALF_CEILING, RoundingMode::HALF_FLOOR => BrickMathRounding::HALF_FLOOR, RoundingMode::HALF_EVEN => BrickMathRounding::HALF_EVEN, ]; public function add(NumberInterface $augend, NumberInterface ...$addends): NumberInterface { $sum = BigInteger::of($augend->toString()); foreach ($addends as $addend) { $sum = $sum->plus($addend->toString()); } return new IntegerObject((string) $sum); } public function subtract(NumberInterface $minuend, NumberInterface ...$subtrahends): NumberInterface { $difference = BigInteger::of($minuend->toString()); foreach ($subtrahends as $subtrahend) { $difference = $difference->minus($subtrahend->toString()); } return new IntegerObject((string) $difference); } public function multiply(NumberInterface $multiplicand, NumberInterface ...$multipliers): NumberInterface { $product = BigInteger::of($multiplicand->toString()); foreach ($multipliers as $multiplier) { $product = $product->multipliedBy($multiplier->toString()); } return new IntegerObject((string) $product); } public function divide( int $roundingMode, int $scale, NumberInterface $dividend, NumberInterface ...$divisors ): NumberInterface { $brickRounding = $this->getBrickRoundingMode($roundingMode); $quotient = BigDecimal::of($dividend->toString()); foreach ($divisors as $divisor) { $quotient = $quotient->dividedBy($divisor->toString(), $scale, $brickRounding); } if ($scale === 0) { return new IntegerObject((string) $quotient->toBigInteger()); } return new Decimal((string) $quotient); } public function fromBase(string $value, int $base): IntegerObject { try { return new IntegerObject((string) BigInteger::fromBase($value, $base)); } catch (MathException | \InvalidArgumentException $exception) { throw new InvalidArgumentException( $exception->getMessage(), (int) $exception->getCode(), $exception ); } } public function toBase(IntegerObject $value, int $base): string { try { return BigInteger::of($value->toString())->toBase($base); } catch (MathException | \InvalidArgumentException $exception) { throw new InvalidArgumentException( $exception->getMessage(), (int) $exception->getCode(), $exception ); } } public function toHexadecimal(IntegerObject $value): Hexadecimal { return new Hexadecimal($this->toBase($value, 16)); } public function toInteger(Hexadecimal $value): IntegerObject { return $this->fromBase($value->toString(), 16); } /** * Maps ramsey/uuid rounding modes to those used by brick/math */ private function getBrickRoundingMode(int $roundingMode): int { return self::ROUNDING_MODE_MAP[$roundingMode] ?? 0; } } Math/RoundingMode.php000064400000011637150251436620010554 0ustar00= 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; } UuidFactory.php000064400000036743150251436620007534 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use DateTimeInterface; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; use Ramsey\Uuid\Generator\DefaultTimeGenerator; use Ramsey\Uuid\Generator\NameGeneratorInterface; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorInterface; use Ramsey\Uuid\Generator\UnixTimeGenerator; use Ramsey\Uuid\Lazy\LazyUuidFromString; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Provider\Time\FixedTimeProvider; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\Time; use Ramsey\Uuid\Validator\ValidatorInterface; use function bin2hex; use function hex2bin; use function pack; use function str_pad; use function strtolower; use function substr; use function substr_replace; use function unpack; use const STR_PAD_LEFT; class UuidFactory implements UuidFactoryInterface { private CodecInterface $codec; private DceSecurityGeneratorInterface $dceSecurityGenerator; private NameGeneratorInterface $nameGenerator; private NodeProviderInterface $nodeProvider; private NumberConverterInterface $numberConverter; private RandomGeneratorInterface $randomGenerator; private TimeConverterInterface $timeConverter; private TimeGeneratorInterface $timeGenerator; private TimeGeneratorInterface $unixTimeGenerator; private UuidBuilderInterface $uuidBuilder; private ValidatorInterface $validator; /** * @var bool whether the feature set was provided from outside, or we can * operate under "default" assumptions */ private bool $isDefaultFeatureSet; /** * @param FeatureSet|null $features A set of available features in the current environment */ public function __construct(?FeatureSet $features = null) { $this->isDefaultFeatureSet = $features === null; $features = $features ?: new FeatureSet(); $this->codec = $features->getCodec(); $this->dceSecurityGenerator = $features->getDceSecurityGenerator(); $this->nameGenerator = $features->getNameGenerator(); $this->nodeProvider = $features->getNodeProvider(); $this->numberConverter = $features->getNumberConverter(); $this->randomGenerator = $features->getRandomGenerator(); $this->timeConverter = $features->getTimeConverter(); $this->timeGenerator = $features->getTimeGenerator(); $this->uuidBuilder = $features->getBuilder(); $this->validator = $features->getValidator(); $this->unixTimeGenerator = $features->getUnixTimeGenerator(); } /** * Returns the codec used by this factory */ public function getCodec(): CodecInterface { return $this->codec; } /** * Sets the codec to use for this factory * * @param CodecInterface $codec A UUID encoder-decoder */ public function setCodec(CodecInterface $codec): void { $this->isDefaultFeatureSet = false; $this->codec = $codec; } /** * Returns the name generator used by this factory */ public function getNameGenerator(): NameGeneratorInterface { return $this->nameGenerator; } /** * Sets the name generator to use for this factory * * @param NameGeneratorInterface $nameGenerator A generator to generate * binary data, based on a namespace and name */ public function setNameGenerator(NameGeneratorInterface $nameGenerator): void { $this->isDefaultFeatureSet = false; $this->nameGenerator = $nameGenerator; } /** * Returns the node provider used by this factory */ public function getNodeProvider(): NodeProviderInterface { return $this->nodeProvider; } /** * Returns the random generator used by this factory */ public function getRandomGenerator(): RandomGeneratorInterface { return $this->randomGenerator; } /** * Returns the time generator used by this factory */ public function getTimeGenerator(): TimeGeneratorInterface { return $this->timeGenerator; } /** * Sets the time generator to use for this factory * * @param TimeGeneratorInterface $generator A generator to generate binary * data, based on the time */ public function setTimeGenerator(TimeGeneratorInterface $generator): void { $this->isDefaultFeatureSet = false; $this->timeGenerator = $generator; } /** * Returns the DCE Security generator used by this factory */ public function getDceSecurityGenerator(): DceSecurityGeneratorInterface { return $this->dceSecurityGenerator; } /** * Sets the DCE Security generator to use for this factory * * @param DceSecurityGeneratorInterface $generator A generator to generate * binary data, based on a local domain and local identifier */ public function setDceSecurityGenerator(DceSecurityGeneratorInterface $generator): void { $this->isDefaultFeatureSet = false; $this->dceSecurityGenerator = $generator; } /** * Returns the number converter used by this factory */ public function getNumberConverter(): NumberConverterInterface { return $this->numberConverter; } /** * Sets the random generator to use for this factory * * @param RandomGeneratorInterface $generator A generator to generate binary * data, based on some random input */ public function setRandomGenerator(RandomGeneratorInterface $generator): void { $this->isDefaultFeatureSet = false; $this->randomGenerator = $generator; } /** * Sets the number converter to use for this factory * * @param NumberConverterInterface $converter A converter to use for working * with large integers (i.e. integers greater than PHP_INT_MAX) */ public function setNumberConverter(NumberConverterInterface $converter): void { $this->isDefaultFeatureSet = false; $this->numberConverter = $converter; } /** * Returns the UUID builder used by this factory */ public function getUuidBuilder(): UuidBuilderInterface { return $this->uuidBuilder; } /** * Sets the UUID builder to use for this factory * * @param UuidBuilderInterface $builder A builder for constructing instances * of UuidInterface */ public function setUuidBuilder(UuidBuilderInterface $builder): void { $this->isDefaultFeatureSet = false; $this->uuidBuilder = $builder; } /** * @psalm-mutation-free */ public function getValidator(): ValidatorInterface { return $this->validator; } /** * Sets the validator to use for this factory * * @param ValidatorInterface $validator A validator to use for validating * whether a string is a valid UUID */ public function setValidator(ValidatorInterface $validator): void { $this->isDefaultFeatureSet = false; $this->validator = $validator; } /** * @psalm-pure */ public function fromBytes(string $bytes): UuidInterface { return $this->codec->decodeBytes($bytes); } /** * @psalm-pure */ public function fromString(string $uuid): UuidInterface { $uuid = strtolower($uuid); return $this->codec->decode($uuid); } /** * @psalm-pure */ public function fromInteger(string $integer): UuidInterface { $hex = $this->numberConverter->toHex($integer); $hex = str_pad($hex, 32, '0', STR_PAD_LEFT); return $this->fromString($hex); } public function fromDateTime( DateTimeInterface $dateTime, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface { $timeProvider = new FixedTimeProvider( new Time($dateTime->format('U'), $dateTime->format('u')) ); $timeGenerator = new DefaultTimeGenerator( $this->nodeProvider, $this->timeConverter, $timeProvider ); $nodeHex = $node ? $node->toString() : null; $bytes = $timeGenerator->generate($nodeHex, $clockSeq); return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_TIME); } /** * @psalm-pure */ public function fromHexadecimal(Hexadecimal $hex): UuidInterface { return $this->codec->decode($hex->__toString()); } /** * @inheritDoc */ public function uuid1($node = null, ?int $clockSeq = null): UuidInterface { $bytes = $this->timeGenerator->generate($node, $clockSeq); return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_TIME); } public function uuid2( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface { $bytes = $this->dceSecurityGenerator->generate( $localDomain, $localIdentifier, $node, $clockSeq ); return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_DCE_SECURITY); } /** * @inheritDoc * @psalm-pure */ public function uuid3($ns, string $name): UuidInterface { return $this->uuidFromNsAndName($ns, $name, Uuid::UUID_TYPE_HASH_MD5, 'md5'); } public function uuid4(): UuidInterface { $bytes = $this->randomGenerator->generate(16); return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_RANDOM); } /** * @inheritDoc * @psalm-pure */ public function uuid5($ns, string $name): UuidInterface { return $this->uuidFromNsAndName($ns, $name, Uuid::UUID_TYPE_HASH_SHA1, 'sha1'); } public function uuid6(?Hexadecimal $node = null, ?int $clockSeq = null): UuidInterface { $nodeHex = $node ? $node->toString() : null; $bytes = $this->timeGenerator->generate($nodeHex, $clockSeq); // Rearrange the bytes, according to the UUID version 6 specification. $v6 = $bytes[6] . $bytes[7] . $bytes[4] . $bytes[5] . $bytes[0] . $bytes[1] . $bytes[2] . $bytes[3]; $v6 = bin2hex($v6); // Drop the first four bits, while adding an empty four bits for the // version field. This allows us to reconstruct the correct time from // the bytes of this UUID. $v6Bytes = hex2bin(substr($v6, 1, 12) . '0' . substr($v6, -3)); $v6Bytes .= substr($bytes, 8); return $this->uuidFromBytesAndVersion($v6Bytes, Uuid::UUID_TYPE_REORDERED_TIME); } /** * Returns a version 7 (Unix Epoch time) UUID * * @param DateTimeInterface|null $dateTime An optional date/time from which * to create the version 7 UUID. If not provided, the UUID is generated * using the current date/time. * * @return UuidInterface A UuidInterface instance that represents a * version 7 UUID */ public function uuid7(?DateTimeInterface $dateTime = null): UuidInterface { assert($this->unixTimeGenerator instanceof UnixTimeGenerator); $bytes = $this->unixTimeGenerator->generate(null, null, $dateTime); return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_UNIX_TIME); } /** * Returns a version 8 (Custom) UUID * * The bytes provided may contain any value according to your application's * needs. Be aware, however, that other applications may not understand the * semantics of the value. * * @param string $bytes A 16-byte octet string. This is an open blob * of data that you may fill with 128 bits of information. Be aware, * however, bits 48 through 51 will be replaced with the UUID version * field, and bits 64 and 65 will be replaced with the UUID variant. You * MUST NOT rely on these bits for your application needs. * * @return UuidInterface A UuidInterface instance that represents a * version 8 UUID */ public function uuid8(string $bytes): UuidInterface { return $this->uuidFromBytesAndVersion($bytes, Uuid::UUID_TYPE_CUSTOM); } /** * Returns a Uuid created from the provided byte string * * Uses the configured builder and codec and the provided byte string to * construct a Uuid object. * * @param string $bytes The byte string from which to construct a UUID * * @return UuidInterface An instance of UuidInterface, created from the * provided bytes * * @psalm-pure */ public function uuid(string $bytes): UuidInterface { /** @psalm-suppress ImpurePropertyFetch */ return $this->uuidBuilder->build($this->codec, $bytes); } /** * Returns a version 3 or 5 namespaced Uuid * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * @param string $name The name to hash together with the namespace * @param int $version The version of UUID to create (3 or 5) * @param string $hashAlgorithm The hashing algorithm to use when hashing * together the namespace and name * * @return UuidInterface An instance of UuidInterface, created by hashing * together the provided namespace and name * * @psalm-pure */ private function uuidFromNsAndName( UuidInterface | string $ns, string $name, int $version, string $hashAlgorithm ): UuidInterface { if (!($ns instanceof UuidInterface)) { $ns = $this->fromString($ns); } $bytes = $this->nameGenerator->generate($ns, $name, $hashAlgorithm); return $this->uuidFromBytesAndVersion(substr($bytes, 0, 16), $version); } /** * Returns an RFC 4122 variant Uuid, created from the provided bytes and version * * @param string $bytes The byte string to convert to a UUID * @param int $version The RFC 4122 version to apply to the UUID * * @return UuidInterface An instance of UuidInterface, created from the * byte string and version * * @psalm-pure */ private function uuidFromBytesAndVersion(string $bytes, int $version): UuidInterface { /** @var array $unpackedTime */ $unpackedTime = unpack('n*', substr($bytes, 6, 2)); $timeHi = (int) $unpackedTime[1]; $timeHiAndVersion = pack('n*', BinaryUtils::applyVersion($timeHi, $version)); /** @var array $unpackedClockSeq */ $unpackedClockSeq = unpack('n*', substr($bytes, 8, 2)); $clockSeqHi = (int) $unpackedClockSeq[1]; $clockSeqHiAndReserved = pack('n*', BinaryUtils::applyVariant($clockSeqHi)); $bytes = substr_replace($bytes, $timeHiAndVersion, 6, 2); $bytes = substr_replace($bytes, $clockSeqHiAndReserved, 8, 2); if ($this->isDefaultFeatureSet) { return LazyUuidFromString::fromBytes($bytes); } /** @psalm-suppress ImpureVariable */ return $this->uuid($bytes); } } Converter/TimeConverterInterface.php000064400000003423150251436620013641 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Time; /** * A time converter converts timestamps into representations that may be used * in UUIDs * * @psalm-immutable */ interface TimeConverterInterface { /** * Uses the provided seconds and micro-seconds to calculate the count of * 100-nanosecond intervals since UTC 00:00:00.00, 15 October 1582, for * RFC 4122 variant UUIDs * * @link http://tools.ietf.org/html/rfc4122#section-4.2.2 RFC 4122, § 4.2.2: Generation Details * * @param string $seconds A string representation of the number of seconds * since the Unix epoch for the time to calculate * @param string $microseconds A string representation of the micro-seconds * associated with the time to calculate * * @return Hexadecimal The full UUID timestamp as a Hexadecimal value * * @psalm-pure */ public function calculateTime(string $seconds, string $microseconds): Hexadecimal; /** * Converts a timestamp extracted from a UUID to a Unix timestamp * * @param Hexadecimal $uuidTimestamp A hexadecimal representation of a UUID * timestamp; a UUID timestamp is a count of 100-nanosecond intervals * since UTC 00:00:00.00, 15 October 1582. * * @return Time An instance of {@see Time} * * @psalm-pure */ public function convertTime(Hexadecimal $uuidTimestamp): Time; } Converter/NumberConverterInterface.php000064400000003052150251436620014171 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter; /** * A number converter converts UUIDs from hexadecimal characters into * representations of integers and vice versa * * @psalm-immutable */ interface NumberConverterInterface { /** * Converts a hexadecimal number into an string integer representation of * the number * * The integer representation returned is a string representation of the * integer, to accommodate unsigned integers greater than PHP_INT_MAX. * * @param string $hex The hexadecimal string representation to convert * * @return string String representation of an integer * * @psalm-return numeric-string * * @psalm-pure */ public function fromHex(string $hex): string; /** * Converts a string integer representation into a hexadecimal string * representation of the number * * @param string $number A string integer representation to convert; this * must be a numeric string to accommodate unsigned integers greater * than PHP_INT_MAX. * * @return string Hexadecimal string * * @psalm-return non-empty-string * * @psalm-pure */ public function toHex(string $number): string; } Converter/Time/GenericTimeConverter.php000064400000007167150251436620014224 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Math\RoundingMode; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\Time; use function explode; use function str_pad; use const STR_PAD_LEFT; /** * GenericTimeConverter uses the provided calculator to calculate and convert * time values * * @psalm-immutable */ class GenericTimeConverter implements TimeConverterInterface { /** * The number of 100-nanosecond intervals from the Gregorian calendar epoch * to the Unix epoch. */ private const GREGORIAN_TO_UNIX_INTERVALS = '122192928000000000'; /** * The number of 100-nanosecond intervals in one second. */ private const SECOND_INTERVALS = '10000000'; /** * The number of 100-nanosecond intervals in one microsecond. */ private const MICROSECOND_INTERVALS = '10'; public function __construct(private CalculatorInterface $calculator) { } public function calculateTime(string $seconds, string $microseconds): Hexadecimal { $timestamp = new Time($seconds, $microseconds); // Convert the seconds into a count of 100-nanosecond intervals. $sec = $this->calculator->multiply( $timestamp->getSeconds(), new IntegerObject(self::SECOND_INTERVALS) ); // Convert the microseconds into a count of 100-nanosecond intervals. $usec = $this->calculator->multiply( $timestamp->getMicroseconds(), new IntegerObject(self::MICROSECOND_INTERVALS) ); // Combine the seconds and microseconds intervals and add the count of // 100-nanosecond intervals from the Gregorian calendar epoch to the // Unix epoch. This gives us the correct count of 100-nanosecond // intervals since the Gregorian calendar epoch for the given seconds // and microseconds. /** @var IntegerObject $uuidTime */ $uuidTime = $this->calculator->add( $sec, $usec, new IntegerObject(self::GREGORIAN_TO_UNIX_INTERVALS) ); $uuidTimeHex = str_pad( $this->calculator->toHexadecimal($uuidTime)->toString(), 16, '0', STR_PAD_LEFT ); return new Hexadecimal($uuidTimeHex); } public function convertTime(Hexadecimal $uuidTimestamp): Time { // From the total, subtract the number of 100-nanosecond intervals from // the Gregorian calendar epoch to the Unix epoch. This gives us the // number of 100-nanosecond intervals from the Unix epoch, which also // includes the microtime. $epochNanoseconds = $this->calculator->subtract( $this->calculator->toInteger($uuidTimestamp), new IntegerObject(self::GREGORIAN_TO_UNIX_INTERVALS) ); // Convert the 100-nanosecond intervals into seconds and microseconds. $unixTimestamp = $this->calculator->divide( RoundingMode::HALF_UP, 6, $epochNanoseconds, new IntegerObject(self::SECOND_INTERVALS) ); $split = explode('.', (string) $unixTimestamp, 2); return new Time($split[0], $split[1] ?? 0); } } Converter/Time/DegradedTimeConverter.php000064400000001147150251436620014337 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Time; /** * @deprecated DegradedTimeConverter is no longer necessary for converting * time on 32-bit systems. Transition to {@see GenericTimeConverter}. * * @psalm-immutable */ class DegradedTimeConverter extends BigNumberTimeConverter { } Converter/Time/PhpTimeConverter.php000064400000012747150251436620013377 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\Time; use function count; use function dechex; use function explode; use function is_float; use function is_int; use function str_pad; use function strlen; use function substr; use const STR_PAD_LEFT; use const STR_PAD_RIGHT; /** * PhpTimeConverter uses built-in PHP functions and standard math operations * available to the PHP programming language to provide facilities for * converting parts of time into representations that may be used in UUIDs * * @psalm-immutable */ class PhpTimeConverter implements TimeConverterInterface { /** * The number of 100-nanosecond intervals from the Gregorian calendar epoch * to the Unix epoch. */ private const GREGORIAN_TO_UNIX_INTERVALS = 0x01b21dd213814000; /** * The number of 100-nanosecond intervals in one second. */ private const SECOND_INTERVALS = 10000000; /** * The number of 100-nanosecond intervals in one microsecond. */ private const MICROSECOND_INTERVALS = 10; private int $phpPrecision; private CalculatorInterface $calculator; private TimeConverterInterface $fallbackConverter; public function __construct( ?CalculatorInterface $calculator = null, ?TimeConverterInterface $fallbackConverter = null ) { if ($calculator === null) { $calculator = new BrickMathCalculator(); } if ($fallbackConverter === null) { $fallbackConverter = new GenericTimeConverter($calculator); } $this->calculator = $calculator; $this->fallbackConverter = $fallbackConverter; $this->phpPrecision = (int) ini_get('precision'); } public function calculateTime(string $seconds, string $microseconds): Hexadecimal { $seconds = new IntegerObject($seconds); $microseconds = new IntegerObject($microseconds); // Calculate the count of 100-nanosecond intervals since the Gregorian // calendar epoch for the given seconds and microseconds. $uuidTime = ((int) $seconds->toString() * self::SECOND_INTERVALS) + ((int) $microseconds->toString() * self::MICROSECOND_INTERVALS) + self::GREGORIAN_TO_UNIX_INTERVALS; // Check to see whether we've overflowed the max/min integer size. // If so, we will default to a different time converter. /** @psalm-suppress RedundantCondition */ if (!is_int($uuidTime)) { return $this->fallbackConverter->calculateTime( $seconds->toString(), $microseconds->toString() ); } return new Hexadecimal(str_pad(dechex($uuidTime), 16, '0', STR_PAD_LEFT)); } public function convertTime(Hexadecimal $uuidTimestamp): Time { $timestamp = $this->calculator->toInteger($uuidTimestamp); // Convert the 100-nanosecond intervals into seconds and microseconds. $splitTime = $this->splitTime( ((int) $timestamp->toString() - self::GREGORIAN_TO_UNIX_INTERVALS) / self::SECOND_INTERVALS ); if (count($splitTime) === 0) { return $this->fallbackConverter->convertTime($uuidTimestamp); } return new Time($splitTime['sec'], $splitTime['usec']); } /** * @param float|int $time The time to split into seconds and microseconds * * @return string[] */ private function splitTime(float | int $time): array { $split = explode('.', (string) $time, 2); // If the $time value is a float but $split only has 1 element, then the // float math was rounded up to the next second, so we want to return // an empty array to allow use of the fallback converter. if (is_float($time) && count($split) === 1) { return []; } if (count($split) === 1) { return [ 'sec' => $split[0], 'usec' => '0', ]; } // If the microseconds are less than six characters AND the length of // the number is greater than or equal to the PHP precision, then it's // possible that we lost some precision for the microseconds. Return an // empty array, so that we can choose to use the fallback converter. if (strlen($split[1]) < 6 && strlen((string) $time) >= $this->phpPrecision) { return []; } $microseconds = $split[1]; // Ensure the microseconds are no longer than 6 digits. If they are, // truncate the number to the first 6 digits and round up, if needed. if (strlen($microseconds) > 6) { $roundingDigit = (int) substr($microseconds, 6, 1); $microseconds = (int) substr($microseconds, 0, 6); if ($roundingDigit >= 5) { $microseconds++; } } return [ 'sec' => $split[0], 'usec' => str_pad((string) $microseconds, 6, '0', STR_PAD_RIGHT), ]; } } Converter/Time/UnixTimeConverter.php000064400000004761150251436620013570 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Math\RoundingMode; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\Time; use function explode; use function str_pad; use const STR_PAD_LEFT; /** * UnixTimeConverter converts Unix Epoch timestamps to/from hexadecimal values * consisting of milliseconds elapsed since the Unix Epoch * * @psalm-immutable */ class UnixTimeConverter implements TimeConverterInterface { private const MILLISECONDS = 1000; public function __construct(private CalculatorInterface $calculator) { } public function calculateTime(string $seconds, string $microseconds): Hexadecimal { $timestamp = new Time($seconds, $microseconds); // Convert the seconds into milliseconds. $sec = $this->calculator->multiply( $timestamp->getSeconds(), new IntegerObject(self::MILLISECONDS), ); // Convert the microseconds into milliseconds; the scale is zero because // we need to discard the fractional part. $usec = $this->calculator->divide( RoundingMode::DOWN, // Always round down to stay in the previous millisecond. 0, $timestamp->getMicroseconds(), new IntegerObject(self::MILLISECONDS), ); /** @var IntegerObject $unixTime */ $unixTime = $this->calculator->add($sec, $usec); $unixTimeHex = str_pad( $this->calculator->toHexadecimal($unixTime)->toString(), 12, '0', STR_PAD_LEFT ); return new Hexadecimal($unixTimeHex); } public function convertTime(Hexadecimal $uuidTimestamp): Time { $milliseconds = $this->calculator->toInteger($uuidTimestamp); $unixTimestamp = $this->calculator->divide( RoundingMode::HALF_UP, 6, $milliseconds, new IntegerObject(self::MILLISECONDS) ); $split = explode('.', (string) $unixTimestamp, 2); return new Time($split[0], $split[1] ?? '0'); } } Converter/Time/BigNumberTimeConverter.php000064400000002454150251436620014514 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Time; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Time; /** * Previously used to integrate moontoast/math as a bignum arithmetic library, * BigNumberTimeConverter is deprecated in favor of GenericTimeConverter * * @deprecated Transition to {@see GenericTimeConverter}. * * @psalm-immutable */ class BigNumberTimeConverter implements TimeConverterInterface { private TimeConverterInterface $converter; public function __construct() { $this->converter = new GenericTimeConverter(new BrickMathCalculator()); } public function calculateTime(string $seconds, string $microseconds): Hexadecimal { return $this->converter->calculateTime($seconds, $microseconds); } public function convertTime(Hexadecimal $uuidTimestamp): Time { return $this->converter->convertTime($uuidTimestamp); } } Converter/Number/BigNumberConverter.php000064400000002406150251436620014224 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Number; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Math\BrickMathCalculator; /** * Previously used to integrate moontoast/math as a bignum arithmetic library, * BigNumberConverter is deprecated in favor of GenericNumberConverter * * @deprecated Transition to {@see GenericNumberConverter}. * * @psalm-immutable */ class BigNumberConverter implements NumberConverterInterface { private NumberConverterInterface $converter; public function __construct() { $this->converter = new GenericNumberConverter(new BrickMathCalculator()); } /** * @inheritDoc * @psalm-pure */ public function fromHex(string $hex): string { return $this->converter->fromHex($hex); } /** * @inheritDoc * @psalm-pure */ public function toHex(string $number): string { return $this->converter->toHex($number); } } Converter/Number/DegradedNumberConverter.php000064400000001156150251436620015223 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Number; /** * @deprecated DegradedNumberConverter is no longer necessary for converting * numbers on 32-bit systems. Transition to {@see GenericNumberConverter}. * * @psalm-immutable */ class DegradedNumberConverter extends BigNumberConverter { } Converter/Number/GenericNumberConverter.php000064400000003336150251436620015102 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Converter\Number; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Type\Integer as IntegerObject; /** * GenericNumberConverter uses the provided calculator to convert decimal * numbers to and from hexadecimal values * * @psalm-immutable */ class GenericNumberConverter implements NumberConverterInterface { public function __construct(private CalculatorInterface $calculator) { } /** * @inheritDoc * @psalm-pure * @psalm-return numeric-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function fromHex(string $hex): string { return $this->calculator->fromBase($hex, 16)->toString(); } /** * @inheritDoc * @psalm-pure * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function toHex(string $number): string { /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ return $this->calculator->toBase(new IntegerObject($number), 16); } } Codec/GuidStringCodec.php000064400000003612150251436620011315 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\Guid\Guid; use Ramsey\Uuid\UuidInterface; use function bin2hex; use function sprintf; use function substr; /** * GuidStringCodec encodes and decodes globally unique identifiers (GUID) * * @see Guid * * @psalm-immutable */ class GuidStringCodec extends StringCodec { public function encode(UuidInterface $uuid): string { $hex = bin2hex($uuid->getFields()->getBytes()); /** @var non-empty-string */ return sprintf( '%02s%02s%02s%02s-%02s%02s-%02s%02s-%04s-%012s', substr($hex, 6, 2), substr($hex, 4, 2), substr($hex, 2, 2), substr($hex, 0, 2), substr($hex, 10, 2), substr($hex, 8, 2), substr($hex, 14, 2), substr($hex, 12, 2), substr($hex, 16, 4), substr($hex, 20), ); } public function decode(string $encodedUuid): UuidInterface { $bytes = $this->getBytes($encodedUuid); return $this->getBuilder()->build($this, $this->swapBytes($bytes)); } public function decodeBytes(string $bytes): UuidInterface { // Specifically call parent::decode to preserve correct byte order return parent::decode(bin2hex($bytes)); } /** * Swaps bytes according to the GUID rules */ private function swapBytes(string $bytes): string { return $bytes[3] . $bytes[2] . $bytes[1] . $bytes[0] . $bytes[5] . $bytes[4] . $bytes[7] . $bytes[6] . substr($bytes, 8); } } Codec/StringCodec.php000064400000006543150251436620010512 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Exception\InvalidUuidStringException; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use function bin2hex; use function hex2bin; use function implode; use function sprintf; use function str_replace; use function strlen; use function substr; /** * StringCodec encodes and decodes RFC 4122 UUIDs * * @link http://tools.ietf.org/html/rfc4122 * * @psalm-immutable */ class StringCodec implements CodecInterface { /** * Constructs a StringCodec * * @param UuidBuilderInterface $builder The builder to use when encoding UUIDs */ public function __construct(private UuidBuilderInterface $builder) { } public function encode(UuidInterface $uuid): string { $hex = bin2hex($uuid->getFields()->getBytes()); /** @var non-empty-string */ return sprintf( '%08s-%04s-%04s-%04s-%012s', substr($hex, 0, 8), substr($hex, 8, 4), substr($hex, 12, 4), substr($hex, 16, 4), substr($hex, 20), ); } /** * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function encodeBinary(UuidInterface $uuid): string { /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ return $uuid->getFields()->getBytes(); } /** * @throws InvalidUuidStringException * * @inheritDoc */ public function decode(string $encodedUuid): UuidInterface { return $this->builder->build($this, $this->getBytes($encodedUuid)); } public function decodeBytes(string $bytes): UuidInterface { if (strlen($bytes) !== 16) { throw new InvalidArgumentException( '$bytes string should contain 16 characters.' ); } return $this->builder->build($this, $bytes); } /** * Returns the UUID builder */ protected function getBuilder(): UuidBuilderInterface { return $this->builder; } /** * Returns a byte string of the UUID */ protected function getBytes(string $encodedUuid): string { $parsedUuid = str_replace( ['urn:', 'uuid:', 'URN:', 'UUID:', '{', '}', '-'], '', $encodedUuid ); $components = [ substr($parsedUuid, 0, 8), substr($parsedUuid, 8, 4), substr($parsedUuid, 12, 4), substr($parsedUuid, 16, 4), substr($parsedUuid, 20), ]; if (!Uuid::isValid(implode('-', $components))) { throw new InvalidUuidStringException( 'Invalid UUID string: ' . $encodedUuid ); } return (string) hex2bin($parsedUuid); } } Codec/OrderedTimeCodec.php000064400000007216150251436620011445 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use function strlen; use function substr; /** * OrderedTimeCodec encodes and decodes a UUID, optimizing the byte order for * more efficient storage * * For binary representations of version 1 UUID, this codec may be used to * reorganize the time fields, making the UUID closer to sequential when storing * the bytes. According to Percona, this optimization can improve database * INSERTs and SELECTs using the UUID column as a key. * * The string representation of the UUID will remain unchanged. Only the binary * representation is reordered. * * **PLEASE NOTE:** Binary representations of UUIDs encoded with this codec must * be decoded with this codec. Decoding using another codec can result in * malformed UUIDs. * * @link https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/ Storing UUID Values in MySQL * * @psalm-immutable */ class OrderedTimeCodec extends StringCodec { /** * Returns a binary string representation of a UUID, with the timestamp * fields rearranged for optimized storage * * @inheritDoc * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function encodeBinary(UuidInterface $uuid): string { if ( !($uuid->getFields() instanceof Rfc4122FieldsInterface) || $uuid->getFields()->getVersion() !== Uuid::UUID_TYPE_TIME ) { throw new InvalidArgumentException( 'Expected RFC 4122 version 1 (time-based) UUID' ); } $bytes = $uuid->getFields()->getBytes(); /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ return $bytes[6] . $bytes[7] . $bytes[4] . $bytes[5] . $bytes[0] . $bytes[1] . $bytes[2] . $bytes[3] . substr($bytes, 8); } /** * Returns a UuidInterface derived from an ordered-time binary string * representation * * @throws InvalidArgumentException if $bytes is an invalid length * * @inheritDoc */ public function decodeBytes(string $bytes): UuidInterface { if (strlen($bytes) !== 16) { throw new InvalidArgumentException( '$bytes string should contain 16 characters.' ); } // Rearrange the bytes to their original order. $rearrangedBytes = $bytes[4] . $bytes[5] . $bytes[6] . $bytes[7] . $bytes[2] . $bytes[3] . $bytes[0] . $bytes[1] . substr($bytes, 8); $uuid = parent::decodeBytes($rearrangedBytes); if ( !($uuid->getFields() instanceof Rfc4122FieldsInterface) || $uuid->getFields()->getVersion() !== Uuid::UUID_TYPE_TIME ) { throw new UnsupportedOperationException( 'Attempting to decode a non-time-based UUID using ' . 'OrderedTimeCodec' ); } return $uuid; } } Codec/CodecInterface.php000064400000004022150251436620011132 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\UuidInterface; /** * A codec encodes and decodes a UUID according to defined rules * * @psalm-immutable */ interface CodecInterface { /** * Returns a hexadecimal string representation of a UuidInterface * * @param UuidInterface $uuid The UUID for which to create a hexadecimal * string representation * * @return string Hexadecimal string representation of a UUID * * @psalm-return non-empty-string */ public function encode(UuidInterface $uuid): string; /** * Returns a binary string representation of a UuidInterface * * @param UuidInterface $uuid The UUID for which to create a binary string * representation * * @return string Binary string representation of a UUID * * @psalm-return non-empty-string */ public function encodeBinary(UuidInterface $uuid): string; /** * Returns a UuidInterface derived from a hexadecimal string representation * * @param string $encodedUuid The hexadecimal string representation to * convert into a UuidInterface instance * * @return UuidInterface An instance of a UUID decoded from a hexadecimal * string representation */ public function decode(string $encodedUuid): UuidInterface; /** * Returns a UuidInterface derived from a binary string representation * * @param string $bytes The binary string representation to convert into a * UuidInterface instance * * @return UuidInterface An instance of a UUID decoded from a binary string * representation */ public function decodeBytes(string $bytes): UuidInterface; } Codec/TimestampFirstCombCodec.php000064400000006516150251436620013020 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\Exception\InvalidUuidStringException; use Ramsey\Uuid\UuidInterface; use function bin2hex; use function sprintf; use function substr; use function substr_replace; /** * TimestampFirstCombCodec encodes and decodes COMBs, with the timestamp as the * first 48 bits * * In contrast with the TimestampLastCombCodec, the TimestampFirstCombCodec * adds the timestamp to the first 48 bits of the COMB. To generate a * timestamp-first COMB, set the TimestampFirstCombCodec as the codec, along * with the CombGenerator as the random generator. * * ``` php * $factory = new UuidFactory(); * * $factory->setCodec(new TimestampFirstCombCodec($factory->getUuidBuilder())); * * $factory->setRandomGenerator(new CombGenerator( * $factory->getRandomGenerator(), * $factory->getNumberConverter() * )); * * $timestampFirstComb = $factory->uuid4(); * ``` * * @link https://www.informit.com/articles/printerfriendly/25862 The Cost of GUIDs as Primary Keys * * @psalm-immutable */ class TimestampFirstCombCodec extends StringCodec { /** * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function encode(UuidInterface $uuid): string { $bytes = $this->swapBytes($uuid->getFields()->getBytes()); return sprintf( '%08s-%04s-%04s-%04s-%012s', bin2hex(substr($bytes, 0, 4)), bin2hex(substr($bytes, 4, 2)), bin2hex(substr($bytes, 6, 2)), bin2hex(substr($bytes, 8, 2)), bin2hex(substr($bytes, 10)) ); } /** * @psalm-return non-empty-string * @psalm-suppress MoreSpecificReturnType we know that the retrieved `string` is never empty * @psalm-suppress LessSpecificReturnStatement we know that the retrieved `string` is never empty */ public function encodeBinary(UuidInterface $uuid): string { /** @phpstan-ignore-next-line PHPStan complains that this is not a non-empty-string. */ return $this->swapBytes($uuid->getFields()->getBytes()); } /** * @throws InvalidUuidStringException * * @inheritDoc */ public function decode(string $encodedUuid): UuidInterface { $bytes = $this->getBytes($encodedUuid); return $this->getBuilder()->build($this, $this->swapBytes($bytes)); } public function decodeBytes(string $bytes): UuidInterface { return $this->getBuilder()->build($this, $this->swapBytes($bytes)); } /** * Swaps bytes according to the timestamp-first COMB rules */ private function swapBytes(string $bytes): string { $first48Bits = substr($bytes, 0, 6); $last48Bits = substr($bytes, -6); $bytes = substr_replace($bytes, $last48Bits, 0, 6); $bytes = substr_replace($bytes, $first48Bits, -6); return $bytes; } } Codec/TimestampLastCombCodec.php000064400000003116150251436620012625 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Codec; /** * TimestampLastCombCodec encodes and decodes COMBs, with the timestamp as the * last 48 bits * * The CombGenerator when used with the StringCodec (and, by proxy, the * TimestampLastCombCodec) adds the timestamp to the last 48 bits of the COMB. * The TimestampLastCombCodec is provided for the sake of consistency. In * practice, it is identical to the standard StringCodec but, it may be used * with the CombGenerator for additional context when reading code. * * Consider the following code. By default, the codec used by UuidFactory is the * StringCodec, but here, we explicitly set the TimestampLastCombCodec. It is * redundant, but it is clear that we intend this COMB to be generated with the * timestamp appearing at the end. * * ``` php * $factory = new UuidFactory(); * * $factory->setCodec(new TimestampLastCombCodec($factory->getUuidBuilder())); * * $factory->setRandomGenerator(new CombGenerator( * $factory->getRandomGenerator(), * $factory->getNumberConverter() * )); * * $timestampLastComb = $factory->uuid4(); * ``` * * @link https://www.informit.com/articles/printerfriendly/25862 The Cost of GUIDs as Primary Keys * * @psalm-immutable */ class TimestampLastCombCodec extends StringCodec { } Provider/TimeProviderInterface.php000064400000001066150251436620013310 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider; use Ramsey\Uuid\Type\Time; /** * A time provider retrieves the current time */ interface TimeProviderInterface { /** * Returns a time object */ public function getTime(): Time; } Provider/DceSecurityProviderInterface.php000064400000001751150251436620014636 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider; use Ramsey\Uuid\Rfc4122\UuidV2; use Ramsey\Uuid\Type\Integer as IntegerObject; /** * A DCE provider provides access to local domain identifiers for version 2, * DCE Security, UUIDs * * @see UuidV2 */ interface DceSecurityProviderInterface { /** * Returns a user identifier for the system * * @link https://en.wikipedia.org/wiki/User_identifier User identifier */ public function getUid(): IntegerObject; /** * Returns a group identifier for the system * * @link https://en.wikipedia.org/wiki/Group_identifier Group identifier */ public function getGid(): IntegerObject; } Provider/Time/SystemTimeProvider.php000064400000001366150251436620013575 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Time; use Ramsey\Uuid\Provider\TimeProviderInterface; use Ramsey\Uuid\Type\Time; use function gettimeofday; /** * SystemTimeProvider retrieves the current time using built-in PHP functions */ class SystemTimeProvider implements TimeProviderInterface { public function getTime(): Time { $time = gettimeofday(); return new Time($time['sec'], $time['usec']); } } Provider/Time/FixedTimeProvider.php000064400000002567150251436620013354 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Time; use Ramsey\Uuid\Provider\TimeProviderInterface; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Type\Time; /** * FixedTimeProvider uses a known time to provide the time * * This provider allows the use of a previously-generated, or known, time * when generating time-based UUIDs. */ class FixedTimeProvider implements TimeProviderInterface { public function __construct(private Time $time) { } /** * Sets the `usec` component of the time * * @param int|string|IntegerObject $value The `usec` value to set */ public function setUsec($value): void { $this->time = new Time($this->time->getSeconds(), $value); } /** * Sets the `sec` component of the time * * @param int|string|IntegerObject $value The `sec` value to set */ public function setSec($value): void { $this->time = new Time($value, $this->time->getMicroseconds()); } public function getTime(): Time { return $this->time; } } Provider/Node/StaticNodeProvider.php000064400000003530150251436620013511 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Node; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use function dechex; use function hexdec; use function str_pad; use function substr; use const STR_PAD_LEFT; /** * StaticNodeProvider provides a static node value with the multicast bit set * * @link http://tools.ietf.org/html/rfc4122#section-4.5 RFC 4122, § 4.5: Node IDs that Do Not Identify the Host */ class StaticNodeProvider implements NodeProviderInterface { private Hexadecimal $node; /** * @param Hexadecimal $node The static node value to use */ public function __construct(Hexadecimal $node) { if (strlen($node->toString()) > 12) { throw new InvalidArgumentException( 'Static node value cannot be greater than 12 hexadecimal characters' ); } $this->node = $this->setMulticastBit($node); } public function getNode(): Hexadecimal { return $this->node; } /** * Set the multicast bit for the static node value */ private function setMulticastBit(Hexadecimal $node): Hexadecimal { $nodeHex = str_pad($node->toString(), 12, '0', STR_PAD_LEFT); $firstOctet = substr($nodeHex, 0, 2); $firstOctet = str_pad( dechex(hexdec($firstOctet) | 0x01), 2, '0', STR_PAD_LEFT ); return new Hexadecimal($firstOctet . substr($nodeHex, 2)); } } Provider/Node/NodeProviderCollection.php000064400000004132150251436620014354 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Node; use Ramsey\Collection\AbstractCollection; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; /** * A collection of NodeProviderInterface objects * * @deprecated this class has been deprecated, and will be removed in 5.0.0. The use-case for this class comes from * a pre-`phpstan/phpstan` and pre-`vimeo/psalm` ecosystem, in which type safety had to be mostly enforced * at runtime: that is no longer necessary, now that you can safely verify your code to be correct, and use * more generic types like `iterable` instead. * * @extends AbstractCollection */ class NodeProviderCollection extends AbstractCollection { public function getType(): string { return NodeProviderInterface::class; } /** * Re-constructs the object from its serialized form * * @param string $serialized The serialized PHP string to unserialize into * a UuidInterface instance * * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @psalm-suppress RedundantConditionGivenDocblockType */ public function unserialize($serialized): void { /** @var array $data */ $data = unserialize($serialized, [ 'allowed_classes' => [ Hexadecimal::class, RandomNodeProvider::class, StaticNodeProvider::class, SystemNodeProvider::class, ], ]); $this->data = array_filter( $data, function ($unserialized): bool { return $unserialized instanceof NodeProviderInterface; } ); } } Provider/Node/SystemNodeProvider.php000064400000011620150251436620013545 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Node; use Ramsey\Uuid\Exception\NodeException; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use function array_filter; use function array_map; use function array_walk; use function count; use function ob_get_clean; use function ob_start; use function preg_match; use function preg_match_all; use function reset; use function str_contains; use function str_replace; use function strtolower; use function strtoupper; use function substr; use const GLOB_NOSORT; use const PREG_PATTERN_ORDER; /** * SystemNodeProvider retrieves the system node ID, if possible * * The system node ID, or host ID, is often the same as the MAC address for a * network interface on the host. */ class SystemNodeProvider implements NodeProviderInterface { /** * Pattern to match nodes in ifconfig and ipconfig output. */ private const IFCONFIG_PATTERN = '/[^:]([0-9a-f]{2}([:-])[0-9a-f]{2}(\2[0-9a-f]{2}){4})[^:]/i'; /** * Pattern to match nodes in sysfs stream output. */ private const SYSFS_PATTERN = '/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i'; public function getNode(): Hexadecimal { $node = $this->getNodeFromSystem(); if ($node === '') { throw new NodeException( 'Unable to fetch a node for this system' ); } return new Hexadecimal($node); } /** * Returns the system node, if it can find it */ protected function getNodeFromSystem(): string { static $node = null; if ($node !== null) { return (string) $node; } // First, try a Linux-specific approach. $node = $this->getSysfs(); if ($node === '') { // Search ifconfig output for MAC addresses & return the first one. $node = $this->getIfconfig(); } $node = str_replace([':', '-'], '', $node); return $node; } /** * Returns the network interface configuration for the system * * @codeCoverageIgnore */ protected function getIfconfig(): string { $disabledFunctions = strtolower((string) ini_get('disable_functions')); if (str_contains($disabledFunctions, 'passthru')) { return ''; } /** * @psalm-suppress UnnecessaryVarAnnotation * @var string $phpOs */ $phpOs = constant('PHP_OS'); ob_start(); switch (strtoupper(substr($phpOs, 0, 3))) { case 'WIN': passthru('ipconfig /all 2>&1'); break; case 'DAR': passthru('ifconfig 2>&1'); break; case 'FRE': passthru('netstat -i -f link 2>&1'); break; case 'LIN': default: passthru('netstat -ie 2>&1'); break; } $ifconfig = (string) ob_get_clean(); if (preg_match_all(self::IFCONFIG_PATTERN, $ifconfig, $matches, PREG_PATTERN_ORDER)) { foreach ($matches[1] as $iface) { if ($iface !== '00:00:00:00:00:00' && $iface !== '00-00-00-00-00-00') { return $iface; } } } return ''; } /** * Returns MAC address from the first system interface via the sysfs interface */ protected function getSysfs(): string { $mac = ''; /** * @psalm-suppress UnnecessaryVarAnnotation * @var string $phpOs */ $phpOs = constant('PHP_OS'); if (strtoupper($phpOs) === 'LINUX') { $addressPaths = glob('/sys/class/net/*/address', GLOB_NOSORT); if ($addressPaths === false || count($addressPaths) === 0) { return ''; } /** @var array $macs */ $macs = []; array_walk($addressPaths, function (string $addressPath) use (&$macs): void { if (is_readable($addressPath)) { $macs[] = file_get_contents($addressPath); } }); /** @var callable $trim */ $trim = 'trim'; $macs = array_map($trim, $macs); // Remove invalid entries. $macs = array_filter($macs, function (string $address) { return $address !== '00:00:00:00:00:00' && preg_match(self::SYSFS_PATTERN, $address); }); /** @var string|bool $mac */ $mac = reset($macs); } return (string) $mac; } } Provider/Node/FallbackNodeProvider.php000064400000002541150251436620013762 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Node; use Ramsey\Uuid\Exception\NodeException; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; /** * FallbackNodeProvider retrieves the system node ID by stepping through a list * of providers until a node ID can be obtained */ class FallbackNodeProvider implements NodeProviderInterface { /** * @param iterable $providers Array of node providers */ public function __construct(private iterable $providers) { } public function getNode(): Hexadecimal { $lastProviderException = null; foreach ($this->providers as $provider) { try { return $provider->getNode(); } catch (NodeException $exception) { $lastProviderException = $exception; continue; } } throw new NodeException( 'Unable to find a suitable node provider', 0, $lastProviderException ); } } Provider/Node/RandomNodeProvider.php000064400000003427150251436620013507 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Node; use Ramsey\Uuid\Exception\RandomSourceException; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use Throwable; use function bin2hex; use function dechex; use function hex2bin; use function hexdec; use function str_pad; use function substr; use const STR_PAD_LEFT; /** * RandomNodeProvider generates a random node ID * * @link http://tools.ietf.org/html/rfc4122#section-4.5 RFC 4122, § 4.5: Node IDs that Do Not Identify the Host */ class RandomNodeProvider implements NodeProviderInterface { public function getNode(): Hexadecimal { try { $nodeBytes = random_bytes(6); } catch (Throwable $exception) { throw new RandomSourceException( $exception->getMessage(), (int) $exception->getCode(), $exception ); } // Split the node bytes for math on 32-bit systems. $nodeMsb = substr($nodeBytes, 0, 3); $nodeLsb = substr($nodeBytes, 3); // Set the multicast bit; see RFC 4122, section 4.5. $nodeMsb = hex2bin( str_pad( dechex(hexdec(bin2hex($nodeMsb)) | 0x010000), 6, '0', STR_PAD_LEFT ) ); // Recombine the node bytes. $node = $nodeMsb . $nodeLsb; return new Hexadecimal(str_pad(bin2hex($node), 12, '0', STR_PAD_LEFT)); } } Provider/NodeProviderInterface.php000064400000001214150251436620013272 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider; use Ramsey\Uuid\Type\Hexadecimal; /** * A node provider retrieves or generates a node ID */ interface NodeProviderInterface { /** * Returns a node ID * * @return Hexadecimal The node ID as a hexadecimal string */ public function getNode(): Hexadecimal; } Provider/Dce/SystemDceSecurityProvider.php000064400000014716150251436620014722 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Provider\Dce; use Ramsey\Uuid\Exception\DceSecurityException; use Ramsey\Uuid\Provider\DceSecurityProviderInterface; use Ramsey\Uuid\Type\Integer as IntegerObject; use function escapeshellarg; use function preg_split; use function str_getcsv; use function strrpos; use function strtolower; use function strtoupper; use function substr; use function trim; use const PREG_SPLIT_NO_EMPTY; /** * SystemDceSecurityProvider retrieves the user or group identifiers from the system */ class SystemDceSecurityProvider implements DceSecurityProviderInterface { /** * @throws DceSecurityException if unable to get a user identifier * * @inheritDoc */ public function getUid(): IntegerObject { /** @var int|float|string|IntegerObject|null $uid */ static $uid = null; if ($uid instanceof IntegerObject) { return $uid; } if ($uid === null) { $uid = $this->getSystemUid(); } if ($uid === '') { throw new DceSecurityException( 'Unable to get a user identifier using the system DCE ' . 'Security provider; please provide a custom identifier or ' . 'use a different provider' ); } $uid = new IntegerObject($uid); return $uid; } /** * @throws DceSecurityException if unable to get a group identifier * * @inheritDoc */ public function getGid(): IntegerObject { /** @var int|float|string|IntegerObject|null $gid */ static $gid = null; if ($gid instanceof IntegerObject) { return $gid; } if ($gid === null) { $gid = $this->getSystemGid(); } if ($gid === '') { throw new DceSecurityException( 'Unable to get a group identifier using the system DCE ' . 'Security provider; please provide a custom identifier or ' . 'use a different provider' ); } $gid = new IntegerObject($gid); return $gid; } /** * Returns the UID from the system */ private function getSystemUid(): string { if (!$this->hasShellExec()) { return ''; } return match ($this->getOs()) { 'WIN' => $this->getWindowsUid(), default => trim((string) shell_exec('id -u')), }; } /** * Returns the GID from the system */ private function getSystemGid(): string { if (!$this->hasShellExec()) { return ''; } return match ($this->getOs()) { 'WIN' => $this->getWindowsGid(), default => trim((string) shell_exec('id -g')), }; } /** * Returns true if shell_exec() is available for use */ private function hasShellExec(): bool { $disabledFunctions = strtolower((string) ini_get('disable_functions')); return !str_contains($disabledFunctions, 'shell_exec'); } /** * Returns the PHP_OS string */ private function getOs(): string { /** * @psalm-suppress UnnecessaryVarAnnotation * @var string $phpOs */ $phpOs = constant('PHP_OS'); return strtoupper(substr($phpOs, 0, 3)); } /** * Returns the user identifier for a user on a Windows system * * Windows does not have the same concept as an effective POSIX UID for the * running script. Instead, each user is uniquely identified by an SID * (security identifier). The SID includes three 32-bit unsigned integers * that make up a unique domain identifier, followed by an RID (relative * identifier) that we will use as the UID. The primary caveat is that this * UID may not be unique to the system, since it is, instead, unique to the * domain. * * @link https://www.lifewire.com/what-is-an-sid-number-2626005 What Is an SID Number? * @link https://bit.ly/30vE7NM Well-known SID Structures * @link https://bit.ly/2FWcYKJ Well-known security identifiers in Windows operating systems * @link https://www.windows-commandline.com/get-sid-of-user/ Get SID of user */ private function getWindowsUid(): string { $response = shell_exec('whoami /user /fo csv /nh'); if ($response === null) { return ''; } $sid = str_getcsv(trim((string) $response))[1] ?? ''; if (($lastHyphen = strrpos($sid, '-')) === false) { return ''; } return trim(substr($sid, $lastHyphen + 1)); } /** * Returns a group identifier for a user on a Windows system * * Since Windows does not have the same concept as an effective POSIX GID * for the running script, we will get the local group memberships for the * user running the script. Then, we will get the SID (security identifier) * for the first group that appears in that list. Finally, we will return * the RID (relative identifier) for the group and use that as the GID. * * @link https://www.windows-commandline.com/list-of-user-groups-command-line/ List of user groups command line */ private function getWindowsGid(): string { $response = shell_exec('net user %username% | findstr /b /i "Local Group Memberships"'); if ($response === null) { return ''; } /** @var string[] $userGroups */ $userGroups = preg_split('/\s{2,}/', (string) $response, -1, PREG_SPLIT_NO_EMPTY); $firstGroup = trim($userGroups[1] ?? '', "* \t\n\r\0\x0B"); if ($firstGroup === '') { return ''; } $response = shell_exec('wmic group get name,sid | findstr /b /i ' . escapeshellarg($firstGroup)); if ($response === null) { return ''; } /** @var string[] $userGroup */ $userGroup = preg_split('/\s{2,}/', (string) $response, -1, PREG_SPLIT_NO_EMPTY); $sid = $userGroup[1] ?? ''; if (($lastHyphen = strrpos($sid, '-')) === false) { return ''; } return trim(substr($sid, $lastHyphen + 1)); } } Fields/SerializableFieldsTrait.php000064400000003717150251436620013240 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Fields; use ValueError; use function base64_decode; use function sprintf; use function strlen; /** * Provides common serialization functionality to fields * * @psalm-immutable */ trait SerializableFieldsTrait { /** * @param string $bytes The bytes that comprise the fields */ abstract public function __construct(string $bytes); /** * Returns the bytes that comprise the fields */ abstract public function getBytes(): string; /** * Returns a string representation of object */ public function serialize(): string { return $this->getBytes(); } /** * @return array{bytes: string} */ public function __serialize(): array { return ['bytes' => $this->getBytes()]; } /** * Constructs the object from a serialized string representation * * @param string $data The serialized string representation of the object * * @psalm-suppress UnusedMethodCall */ public function unserialize(string $data): void { if (strlen($data) === 16) { $this->__construct($data); } else { $this->__construct(base64_decode($data)); } } /** * @param array{bytes?: string} $data * * @psalm-suppress UnusedMethodCall */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['bytes'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['bytes']); } } Fields/FieldsInterface.php000064400000001354150251436620011521 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Fields; use Serializable; /** * UUIDs are comprised of unsigned integers, the bytes of which are separated * into fields and arranged in a particular layout defined by the specification * for the variant * * @psalm-immutable */ interface FieldsInterface extends Serializable { /** * Returns the bytes that comprise the fields */ public function getBytes(): string; } Uuid.php000064400000062730150251436620006177 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use BadMethodCallException; use DateTimeInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Fields\FieldsInterface; use Ramsey\Uuid\Lazy\LazyUuidFromString; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use ValueError; use function assert; use function bin2hex; use function method_exists; use function preg_match; use function sprintf; use function str_replace; use function strcmp; use function strlen; use function strtolower; use function substr; /** * Uuid provides constants and static methods for working with and generating UUIDs * * @psalm-immutable */ class Uuid implements UuidInterface { use DeprecatedUuidMethodsTrait; /** * When this namespace is specified, the name string is a fully-qualified * domain name * * @link http://tools.ietf.org/html/rfc4122#appendix-C RFC 4122, Appendix C: Some Name Space IDs */ public const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; /** * When this namespace is specified, the name string is a URL * * @link http://tools.ietf.org/html/rfc4122#appendix-C RFC 4122, Appendix C: Some Name Space IDs */ public const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; /** * When this namespace is specified, the name string is an ISO OID * * @link http://tools.ietf.org/html/rfc4122#appendix-C RFC 4122, Appendix C: Some Name Space IDs */ public const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; /** * When this namespace is specified, the name string is an X.500 DN in DER * or a text output format * * @link http://tools.ietf.org/html/rfc4122#appendix-C RFC 4122, Appendix C: Some Name Space IDs */ public const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; /** * The nil UUID is a special form of UUID that is specified to have all 128 * bits set to zero * * @link http://tools.ietf.org/html/rfc4122#section-4.1.7 RFC 4122, § 4.1.7: Nil UUID */ public const NIL = '00000000-0000-0000-0000-000000000000'; /** * The max UUID is a special form of UUID that is specified to have all 128 * bits set to one * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.10 Max UUID */ public const MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; /** * Variant: reserved, NCS backward compatibility * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant */ public const RESERVED_NCS = 0; /** * Variant: the UUID layout specified in RFC 4122 * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant */ public const RFC_4122 = 2; /** * Variant: reserved, Microsoft Corporation backward compatibility * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant */ public const RESERVED_MICROSOFT = 6; /** * Variant: reserved for future definition * * @link http://tools.ietf.org/html/rfc4122#section-4.1.1 RFC 4122, § 4.1.1: Variant */ public const RESERVED_FUTURE = 7; /** * @deprecated Use {@see ValidatorInterface::getPattern()} instead. */ public const VALID_PATTERN = '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$'; /** * Version 1 (Gregorian time) UUID * * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public const UUID_TYPE_TIME = 1; /** * Version 2 (DCE Security) UUID * * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public const UUID_TYPE_DCE_SECURITY = 2; /** * @deprecated Use {@see Uuid::UUID_TYPE_DCE_SECURITY} instead. */ public const UUID_TYPE_IDENTIFIER = 2; /** * Version 3 (name-based and hashed with MD5) UUID * * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public const UUID_TYPE_HASH_MD5 = 3; /** * Version 4 (random) UUID * * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public const UUID_TYPE_RANDOM = 4; /** * Version 5 (name-based and hashed with SHA1) UUID * * @link https://tools.ietf.org/html/rfc4122#section-4.1.3 RFC 4122, § 4.1.3: Version */ public const UUID_TYPE_HASH_SHA1 = 5; /** * @deprecated Use {@see Uuid::UUID_TYPE_REORDERED_TIME} instead. */ public const UUID_TYPE_PEABODY = 6; /** * Version 6 (reordered time) UUID * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.6 UUID Version 6 */ public const UUID_TYPE_REORDERED_TIME = 6; /** * Version 7 (Unix Epoch time) UUID * * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.7 UUID Version 7 */ public const UUID_TYPE_UNIX_TIME = 7; /** * @link https://datatracker.ietf.org/doc/html/draft-ietf-uuidrev-rfc4122bis-00#section-5.8 UUID Version 8 */ public const UUID_TYPE_CUSTOM = 8; /** * DCE Security principal domain * * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 */ public const DCE_DOMAIN_PERSON = 0; /** * DCE Security group domain * * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 */ public const DCE_DOMAIN_GROUP = 1; /** * DCE Security organization domain * * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 */ public const DCE_DOMAIN_ORG = 2; /** * DCE Security domain string names * * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 */ public const DCE_DOMAIN_NAMES = [ self::DCE_DOMAIN_PERSON => 'person', self::DCE_DOMAIN_GROUP => 'group', self::DCE_DOMAIN_ORG => 'org', ]; private static ?UuidFactoryInterface $factory = null; /** * @var bool flag to detect if the UUID factory was replaced internally, * which disables all optimizations for the default/happy path internal * scenarios */ private static bool $factoryReplaced = false; protected CodecInterface $codec; protected NumberConverterInterface $numberConverter; protected Rfc4122FieldsInterface $fields; protected TimeConverterInterface $timeConverter; /** * Creates a universally unique identifier (UUID) from an array of fields * * Unless you're making advanced use of this library to generate identifiers * that deviate from RFC 4122, you probably do not want to instantiate a * UUID directly. Use the static methods, instead: * * ``` * use Ramsey\Uuid\Uuid; * * $timeBasedUuid = Uuid::uuid1(); * $namespaceMd5Uuid = Uuid::uuid3(Uuid::NAMESPACE_URL, 'http://php.net/'); * $randomUuid = Uuid::uuid4(); * $namespaceSha1Uuid = Uuid::uuid5(Uuid::NAMESPACE_URL, 'http://php.net/'); * ``` * * @param Rfc4122FieldsInterface $fields The fields from which to construct a UUID * @param NumberConverterInterface $numberConverter The number converter to use * for converting hex values to/from integers * @param CodecInterface $codec The codec to use when encoding or decoding * UUID strings * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to unix timestamps */ public function __construct( Rfc4122FieldsInterface $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { $this->fields = $fields; $this->codec = $codec; $this->numberConverter = $numberConverter; $this->timeConverter = $timeConverter; } /** * @psalm-return non-empty-string */ public function __toString(): string { return $this->toString(); } /** * Converts the UUID to a string for JSON serialization */ public function jsonSerialize(): string { return $this->toString(); } /** * Converts the UUID to a string for PHP serialization */ public function serialize(): string { return $this->getFields()->getBytes(); } /** * @return array{bytes: string} */ public function __serialize(): array { return ['bytes' => $this->serialize()]; } /** * Re-constructs the object from its serialized form * * @param string $data The serialized PHP string to unserialize into * a UuidInterface instance */ public function unserialize(string $data): void { if (strlen($data) === 16) { /** @var Uuid $uuid */ $uuid = self::getFactory()->fromBytes($data); } else { /** @var Uuid $uuid */ $uuid = self::getFactory()->fromString($data); } $this->codec = $uuid->codec; $this->numberConverter = $uuid->numberConverter; $this->fields = $uuid->fields; $this->timeConverter = $uuid->timeConverter; } /** * @param array{bytes?: string} $data */ public function __unserialize(array $data): void { // @codeCoverageIgnoreStart if (!isset($data['bytes'])) { throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); } // @codeCoverageIgnoreEnd $this->unserialize($data['bytes']); } public function compareTo(UuidInterface $other): int { $compare = strcmp($this->toString(), $other->toString()); if ($compare < 0) { return -1; } if ($compare > 0) { return 1; } return 0; } public function equals(?object $other): bool { if (!$other instanceof UuidInterface) { return false; } return $this->compareTo($other) === 0; } /** * @psalm-return non-empty-string */ public function getBytes(): string { return $this->codec->encodeBinary($this); } public function getFields(): FieldsInterface { return $this->fields; } public function getHex(): Hexadecimal { return new Hexadecimal(str_replace('-', '', $this->toString())); } public function getInteger(): IntegerObject { return new IntegerObject($this->numberConverter->fromHex($this->getHex()->toString())); } public function getUrn(): string { return 'urn:uuid:' . $this->toString(); } /** * @psalm-return non-empty-string */ public function toString(): string { return $this->codec->encode($this); } /** * Returns the factory used to create UUIDs */ public static function getFactory(): UuidFactoryInterface { if (self::$factory === null) { self::$factory = new UuidFactory(); } return self::$factory; } /** * Sets the factory used to create UUIDs * * @param UuidFactoryInterface $factory A factory that will be used by this * class to create UUIDs */ public static function setFactory(UuidFactoryInterface $factory): void { // Note: non-strict equality is intentional here. If the factory is configured differently, every assumption // around purity is broken, and we have to internally decide everything differently. // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator self::$factoryReplaced = ($factory != new UuidFactory()); self::$factory = $factory; } /** * Creates a UUID from a byte string * * @param string $bytes A binary string * * @return UuidInterface A UuidInterface instance created from a binary * string representation * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners * * @psalm-suppress ImpureStaticProperty we know that the factory being replaced can lead to massive * havoc across all consumers: that should never happen, and * is generally to be discouraged. Until the factory is kept * un-replaced, this method is effectively pure. */ public static function fromBytes(string $bytes): UuidInterface { if (! self::$factoryReplaced && strlen($bytes) === 16) { $base16Uuid = bin2hex($bytes); // Note: we are calling `fromString` internally because we don't know if the given `$bytes` is a valid UUID return self::fromString( substr($base16Uuid, 0, 8) . '-' . substr($base16Uuid, 8, 4) . '-' . substr($base16Uuid, 12, 4) . '-' . substr($base16Uuid, 16, 4) . '-' . substr($base16Uuid, 20, 12) ); } return self::getFactory()->fromBytes($bytes); } /** * Creates a UUID from the string standard representation * * @param string $uuid A hexadecimal string * * @return UuidInterface A UuidInterface instance created from a hexadecimal * string representation * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners * * @psalm-suppress ImpureStaticProperty we know that the factory being replaced can lead to massive * havoc across all consumers: that should never happen, and * is generally to be discouraged. Until the factory is kept * un-replaced, this method is effectively pure. */ public static function fromString(string $uuid): UuidInterface { $uuid = strtolower($uuid); if (! self::$factoryReplaced && preg_match(LazyUuidFromString::VALID_REGEX, $uuid) === 1) { assert($uuid !== ''); return new LazyUuidFromString($uuid); } return self::getFactory()->fromString($uuid); } /** * Creates a UUID from a DateTimeInterface instance * * @param DateTimeInterface $dateTime The date and time * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return UuidInterface A UuidInterface instance that represents a * version 1 UUID created from a DateTimeInterface instance */ public static function fromDateTime( DateTimeInterface $dateTime, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface { return self::getFactory()->fromDateTime($dateTime, $node, $clockSeq); } /** * Creates a UUID from the Hexadecimal object * * @param Hexadecimal $hex Hexadecimal object representing a hexadecimal number * * @return UuidInterface A UuidInterface instance created from the Hexadecimal * object representing a hexadecimal number * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners * @psalm-suppress MixedInferredReturnType,MixedReturnStatement */ public static function fromHexadecimal(Hexadecimal $hex): UuidInterface { $factory = self::getFactory(); if (method_exists($factory, 'fromHexadecimal')) { /** * @phpstan-ignore-next-line * @psalm-suppress UndefinedInterfaceMethod */ return self::getFactory()->fromHexadecimal($hex); } throw new BadMethodCallException('The method fromHexadecimal() does not exist on the provided factory'); } /** * Creates a UUID from a 128-bit integer string * * @param string $integer String representation of 128-bit integer * * @return UuidInterface A UuidInterface instance created from the string * representation of a 128-bit integer * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners */ public static function fromInteger(string $integer): UuidInterface { /** @psalm-suppress ImpureMethodCall */ return self::getFactory()->fromInteger($integer); } /** * Returns true if the provided string is a valid UUID * * @param string $uuid A string to validate as a UUID * * @return bool True if the string is a valid UUID, false otherwise * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners * * @psalm-assert-if-true =non-empty-string $uuid */ public static function isValid(string $uuid): bool { /** @psalm-suppress ImpureMethodCall */ return self::getFactory()->getValidator()->validate($uuid); } /** * Returns a version 1 (Gregorian time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|int|string|null $node A 48-bit number representing the * hardware address; this number may be represented as an integer or a * hexadecimal string * @param int|null $clockSeq A 14-bit number used to help avoid duplicates that * could arise when the clock is set backwards in time or if the node ID * changes * * @return UuidInterface A UuidInterface instance that represents a * version 1 UUID */ public static function uuid1($node = null, ?int $clockSeq = null): UuidInterface { return self::getFactory()->uuid1($node, $clockSeq); } /** * Returns a version 2 (DCE Security) UUID from a local domain, local * identifier, host ID, clock sequence, and the current time * * @param int $localDomain The local domain to use when generating bytes, * according to DCE Security * @param IntegerObject|null $localIdentifier The local identifier for the * given domain; this may be a UID or GID on POSIX systems, if the local * domain is person or group, or it may be a site-defined identifier * if the local domain is org * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes (in a version 2 UUID, the lower 8 bits of this number * are replaced with the domain). * * @return UuidInterface A UuidInterface instance that represents a * version 2 UUID */ public static function uuid2( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface { return self::getFactory()->uuid2($localDomain, $localIdentifier, $node, $clockSeq); } /** * Returns a version 3 (name-based) UUID based on the MD5 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * @param string $name The name to use for creating a UUID * * @return UuidInterface A UuidInterface instance that represents a * version 3 UUID * * @psalm-suppress ImpureMethodCall we know that the factory being replaced can lead to massive * havoc across all consumers: that should never happen, and * is generally to be discouraged. Until the factory is kept * un-replaced, this method is effectively pure. * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners */ public static function uuid3($ns, string $name): UuidInterface { return self::getFactory()->uuid3($ns, $name); } /** * Returns a version 4 (random) UUID * * @return UuidInterface A UuidInterface instance that represents a * version 4 UUID */ public static function uuid4(): UuidInterface { return self::getFactory()->uuid4(); } /** * Returns a version 5 (name-based) UUID based on the SHA-1 hash of a * namespace ID and a name * * @param string|UuidInterface $ns The namespace (must be a valid UUID) * @param string $name The name to use for creating a UUID * * @return UuidInterface A UuidInterface instance that represents a * version 5 UUID * * @psalm-pure note: changing the internal factory is an edge case not covered by purity invariants, * but under constant factory setups, this method operates in functionally pure manners * * @psalm-suppress ImpureMethodCall we know that the factory being replaced can lead to massive * havoc across all consumers: that should never happen, and * is generally to be discouraged. Until the factory is kept * un-replaced, this method is effectively pure. */ public static function uuid5($ns, string $name): UuidInterface { return self::getFactory()->uuid5($ns, $name); } /** * Returns a version 6 (reordered time) UUID from a host ID, sequence number, * and the current time * * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates that * could arise when the clock is set backwards in time or if the node ID * changes * * @return UuidInterface A UuidInterface instance that represents a * version 6 UUID */ public static function uuid6( ?Hexadecimal $node = null, ?int $clockSeq = null ): UuidInterface { return self::getFactory()->uuid6($node, $clockSeq); } /** * Returns a version 7 (Unix Epoch time) UUID * * @param DateTimeInterface|null $dateTime An optional date/time from which * to create the version 7 UUID. If not provided, the UUID is generated * using the current date/time. * * @return UuidInterface A UuidInterface instance that represents a * version 7 UUID */ public static function uuid7(?DateTimeInterface $dateTime = null): UuidInterface { $factory = self::getFactory(); if (method_exists($factory, 'uuid7')) { /** @var UuidInterface */ return $factory->uuid7($dateTime); } throw new UnsupportedOperationException( 'The provided factory does not support the uuid7() method', ); } /** * Returns a version 8 (custom) UUID * * The bytes provided may contain any value according to your application's * needs. Be aware, however, that other applications may not understand the * semantics of the value. * * @param string $bytes A 16-byte octet string. This is an open blob * of data that you may fill with 128 bits of information. Be aware, * however, bits 48 through 51 will be replaced with the UUID version * field, and bits 64 and 65 will be replaced with the UUID variant. You * MUST NOT rely on these bits for your application needs. * * @return UuidInterface A UuidInterface instance that represents a * version 8 UUID */ public static function uuid8(string $bytes): UuidInterface { $factory = self::getFactory(); if (method_exists($factory, 'uuid8')) { /** @var UuidInterface */ return $factory->uuid8($bytes); } throw new UnsupportedOperationException( 'The provided factory does not support the uuid8() method', ); } } Builder/UuidBuilderInterface.php000064400000002017150251436620012705 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Builder; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\UuidInterface; /** * A UUID builder builds instances of UuidInterface * * @psalm-immutable */ interface UuidBuilderInterface { /** * Builds and returns a UuidInterface * * @param CodecInterface $codec The codec to use for building this UuidInterface instance * @param string $bytes The byte string from which to construct a UUID * * @return UuidInterface Implementations may choose to return more specific * instances of UUIDs that implement UuidInterface * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface; } Builder/DegradedUuidBuilder.php000064400000004114150251436620012504 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Builder; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\DegradedTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\DegradedUuid; use Ramsey\Uuid\Rfc4122\Fields as Rfc4122Fields; use Ramsey\Uuid\UuidInterface; /** * @deprecated DegradedUuid instances are no longer necessary to support 32-bit * systems. Transition to {@see DefaultUuidBuilder}. * * @psalm-immutable */ class DegradedUuidBuilder implements UuidBuilderInterface { private TimeConverterInterface $timeConverter; /** * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the DegradedUuid * @param TimeConverterInterface|null $timeConverter The time converter to use * for converting timestamps extracted from a UUID to Unix timestamps */ public function __construct( private NumberConverterInterface $numberConverter, ?TimeConverterInterface $timeConverter = null ) { $this->timeConverter = $timeConverter ?: new DegradedTimeConverter(); } /** * Builds and returns a DegradedUuid * * @param CodecInterface $codec The codec to use for building this DegradedUuid instance * @param string $bytes The byte string from which to construct a UUID * * @return DegradedUuid The DegradedUuidBuild returns an instance of Ramsey\Uuid\DegradedUuid * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface { return new DegradedUuid( new Rfc4122Fields($bytes), $this->numberConverter, $codec, $this->timeConverter ); } } Builder/BuilderCollection.php000064400000005336150251436620012260 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Builder; use Ramsey\Collection\AbstractCollection; use Ramsey\Uuid\Converter\Number\GenericNumberConverter; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Converter\Time\PhpTimeConverter; use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Nonstandard\UuidBuilder as NonstandardUuidBuilder; use Ramsey\Uuid\Rfc4122\UuidBuilder as Rfc4122UuidBuilder; use Traversable; /** * A collection of UuidBuilderInterface objects * * @deprecated this class has been deprecated, and will be removed in 5.0.0. The use-case for this class comes from * a pre-`phpstan/phpstan` and pre-`vimeo/psalm` ecosystem, in which type safety had to be mostly enforced * at runtime: that is no longer necessary, now that you can safely verify your code to be correct, and use * more generic types like `iterable` instead. * * @extends AbstractCollection */ class BuilderCollection extends AbstractCollection { public function getType(): string { return UuidBuilderInterface::class; } /** * @psalm-mutation-free * @psalm-suppress ImpureMethodCall * @psalm-suppress InvalidTemplateParam */ public function getIterator(): Traversable { return parent::getIterator(); } /** * Re-constructs the object from its serialized form * * @param string $serialized The serialized PHP string to unserialize into * a UuidInterface instance * * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @psalm-suppress RedundantConditionGivenDocblockType */ public function unserialize($serialized): void { /** @var array $data */ $data = unserialize($serialized, [ 'allowed_classes' => [ BrickMathCalculator::class, GenericNumberConverter::class, GenericTimeConverter::class, GuidBuilder::class, NonstandardUuidBuilder::class, PhpTimeConverter::class, Rfc4122UuidBuilder::class, ], ]); $this->data = array_filter( $data, function ($unserialized): bool { return $unserialized instanceof UuidBuilderInterface; } ); } } Builder/FallbackBuilder.php000064400000003555150251436620011665 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Builder; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Exception\BuilderNotFoundException; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\UuidInterface; /** * FallbackBuilder builds a UUID by stepping through a list of UUID builders * until a UUID can be constructed without exceptions * * @psalm-immutable */ class FallbackBuilder implements UuidBuilderInterface { /** * @param iterable $builders An array of UUID builders */ public function __construct(private iterable $builders) { } /** * Builds and returns a UuidInterface instance using the first builder that * succeeds * * @param CodecInterface $codec The codec to use for building this instance * @param string $bytes The byte string from which to construct a UUID * * @return UuidInterface an instance of a UUID object * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface { $lastBuilderException = null; foreach ($this->builders as $builder) { try { return $builder->build($codec, $bytes); } catch (UnableToBuildUuidException $exception) { $lastBuilderException = $exception; continue; } } throw new BuilderNotFoundException( 'Could not find a suitable builder for the provided codec and fields', 0, $lastBuilderException ); } } Builder/DefaultUuidBuilder.php000064400000001070150251436620012367 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Builder; use Ramsey\Uuid\Rfc4122\UuidBuilder as Rfc4122UuidBuilder; /** * @deprecated Transition to {@see Rfc4122UuidBuilder}. * * @psalm-immutable */ class DefaultUuidBuilder extends Rfc4122UuidBuilder { } Guid/GuidBuilder.php000064400000004212150251436620010347 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Guid; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\UuidInterface; use Throwable; /** * GuidBuilder builds instances of Guid * * @see Guid * * @psalm-immutable */ class GuidBuilder implements UuidBuilderInterface { /** * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the Guid * @param TimeConverterInterface $timeConverter The time converter to use * for converting timestamps extracted from a UUID to Unix timestamps */ public function __construct( private NumberConverterInterface $numberConverter, private TimeConverterInterface $timeConverter ) { } /** * Builds and returns a Guid * * @param CodecInterface $codec The codec to use for building this Guid instance * @param string $bytes The byte string from which to construct a UUID * * @return Guid The GuidBuilder returns an instance of Ramsey\Uuid\Guid\Guid * * @psalm-pure */ public function build(CodecInterface $codec, string $bytes): UuidInterface { try { return new Guid( $this->buildFields($bytes), $this->numberConverter, $codec, $this->timeConverter ); } catch (Throwable $e) { throw new UnableToBuildUuidException($e->getMessage(), (int) $e->getCode(), $e); } } /** * Proxy method to allow injecting a mock, for testing */ protected function buildFields(string $bytes): Fields { return new Fields($bytes); } } Guid/Guid.php000064400000004377150251436620007054 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Guid; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Uuid; /** * Guid represents a UUID with "native" (little-endian) byte order * * From Wikipedia: * * > The first three fields are unsigned 32- and 16-bit integers and are subject * > to swapping, while the last two fields consist of uninterpreted bytes, not * > subject to swapping. This byte swapping applies even for versions 3, 4, and * > 5, where the canonical fields do not correspond to the content of the UUID. * * The first three fields of a GUID are encoded in little-endian byte order, * while the last three fields are in network (big-endian) byte order. This is * according to the history of the Microsoft definition of a GUID. * * According to the .NET Guid.ToByteArray method documentation: * * > Note that the order of bytes in the returned byte array is different from * > the string representation of a Guid value. The order of the beginning * > four-byte group and the next two two-byte groups is reversed, whereas the * > order of the last two-byte group and the closing six-byte group is the * > same. * * @link https://en.wikipedia.org/wiki/Universally_unique_identifier#Variants UUID Variants on Wikipedia * @link https://docs.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid Windows GUID structure * @link https://docs.microsoft.com/en-us/dotnet/api/system.guid .NET Guid Struct * @link https://docs.microsoft.com/en-us/dotnet/api/system.guid.tobytearray .NET Guid.ToByteArray Method * * @psalm-immutable */ final class Guid extends Uuid { public function __construct( Fields $fields, NumberConverterInterface $numberConverter, CodecInterface $codec, TimeConverterInterface $timeConverter ) { parent::__construct($fields, $numberConverter, $codec, $timeConverter); } } Guid/Fields.php000064400000012133150251436620007357 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Guid; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Fields\SerializableFieldsTrait; use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\Rfc4122\MaxTrait; use Ramsey\Uuid\Rfc4122\NilTrait; use Ramsey\Uuid\Rfc4122\VariantTrait; use Ramsey\Uuid\Rfc4122\VersionTrait; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Uuid; use function bin2hex; use function dechex; use function hexdec; use function pack; use function sprintf; use function str_pad; use function strlen; use function substr; use function unpack; use const STR_PAD_LEFT; /** * GUIDs are comprised of a set of named fields, according to RFC 4122 * * @see Guid * * @psalm-immutable */ final class Fields implements FieldsInterface { use MaxTrait; use NilTrait; use SerializableFieldsTrait; use VariantTrait; use VersionTrait; /** * @param string $bytes A 16-byte binary string representation of a UUID * * @throws InvalidArgumentException if the byte string is not exactly 16 bytes * @throws InvalidArgumentException if the byte string does not represent a GUID * @throws InvalidArgumentException if the byte string does not contain a valid version */ public function __construct(private string $bytes) { if (strlen($this->bytes) !== 16) { throw new InvalidArgumentException( 'The byte string must be 16 bytes long; ' . 'received ' . strlen($this->bytes) . ' bytes' ); } if (!$this->isCorrectVariant()) { throw new InvalidArgumentException( 'The byte string received does not conform to the RFC ' . '4122 or Microsoft Corporation variants' ); } if (!$this->isCorrectVersion()) { throw new InvalidArgumentException( 'The byte string received does not contain a valid version' ); } } public function getBytes(): string { return $this->bytes; } public function getTimeLow(): Hexadecimal { // Swap the bytes from little endian to network byte order. /** @var array $hex */ $hex = unpack( 'H*', pack( 'v*', hexdec(bin2hex(substr($this->bytes, 2, 2))), hexdec(bin2hex(substr($this->bytes, 0, 2))) ) ); return new Hexadecimal((string) ($hex[1] ?? '')); } public function getTimeMid(): Hexadecimal { // Swap the bytes from little endian to network byte order. /** @var array $hex */ $hex = unpack( 'H*', pack( 'v', hexdec(bin2hex(substr($this->bytes, 4, 2))) ) ); return new Hexadecimal((string) ($hex[1] ?? '')); } public function getTimeHiAndVersion(): Hexadecimal { // Swap the bytes from little endian to network byte order. /** @var array $hex */ $hex = unpack( 'H*', pack( 'v', hexdec(bin2hex(substr($this->bytes, 6, 2))) ) ); return new Hexadecimal((string) ($hex[1] ?? '')); } public function getTimestamp(): Hexadecimal { return new Hexadecimal(sprintf( '%03x%04s%08s', hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, $this->getTimeMid()->toString(), $this->getTimeLow()->toString() )); } public function getClockSeq(): Hexadecimal { if ($this->isMax()) { $clockSeq = 0xffff; } elseif ($this->isNil()) { $clockSeq = 0x0000; } else { $clockSeq = hexdec(bin2hex(substr($this->bytes, 8, 2))) & 0x3fff; } return new Hexadecimal(str_pad(dechex($clockSeq), 4, '0', STR_PAD_LEFT)); } public function getClockSeqHiAndReserved(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 8, 1))); } public function getClockSeqLow(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 9, 1))); } public function getNode(): Hexadecimal { return new Hexadecimal(bin2hex(substr($this->bytes, 10))); } public function getVersion(): ?int { if ($this->isNil() || $this->isMax()) { return null; } /** @var array $parts */ $parts = unpack('n*', $this->bytes); return ((int) $parts[4] >> 4) & 0x00f; } private function isCorrectVariant(): bool { if ($this->isNil() || $this->isMax()) { return true; } $variant = $this->getVariant(); return $variant === Uuid::RFC_4122 || $variant === Uuid::RESERVED_MICROSOFT; } } DeprecatedUuidInterface.php000064400000012055150251436620011774 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use DateTimeInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; /** * This interface encapsulates deprecated methods for ramsey/uuid * * @psalm-immutable */ interface DeprecatedUuidInterface { /** * @deprecated This method will be removed in 5.0.0. There is no alternative * recommendation, so plan accordingly. */ public function getNumberConverter(): NumberConverterInterface; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. * * @return string[] */ public function getFieldsHex(): array; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqHiAndReserved()}. */ public function getClockSeqHiAndReservedHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeqLow()}. */ public function getClockSeqLowHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getClockSeq()}. */ public function getClockSequenceHex(): string; /** * @deprecated In ramsey/uuid version 5.0.0, this will be removed from the * interface. It is available at {@see UuidV1::getDateTime()}. */ public function getDateTime(): DateTimeInterface; /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getLeastSignificantBitsHex(): string; /** * @deprecated This method will be removed in 5.0.0. There is no direct * alternative, but the same information may be obtained by splitting * in half the value returned by {@see UuidInterface::getHex()}. */ public function getMostSignificantBitsHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getNode()}. */ public function getNodeHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeHiAndVersion()}. */ public function getTimeHiAndVersionHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeLow()}. */ public function getTimeLowHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimeMid()}. */ public function getTimeMidHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getTimestamp()}. */ public function getTimestampHex(): string; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getVariant()}. */ public function getVariant(): ?int; /** * @deprecated Use {@see UuidInterface::getFields()} to get a * {@see FieldsInterface} instance. If it is a * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface} instance, you may call * {@see \Ramsey\Uuid\Rfc4122\FieldsInterface::getVersion()}. */ public function getVersion(): ?int; } Generator/DceSecurityGeneratorInterface.php000064400000003324150251436620015124 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Rfc4122\UuidV2; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; /** * A DCE Security generator generates strings of binary data based on a local * domain, local identifier, node ID, clock sequence, and the current time * * @see UuidV2 */ interface DceSecurityGeneratorInterface { /** * Generate a binary string from a local domain, local identifier, node ID, * clock sequence, and current time * * @param int $localDomain The local domain to use when generating bytes, * according to DCE Security * @param IntegerObject|null $localIdentifier The local identifier for the * given domain; this may be a UID or GID on POSIX systems, if the local * domain is person or group, or it may be a site-defined identifier * if the local domain is org * @param Hexadecimal|null $node A 48-bit number representing the hardware * address * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return string A binary string */ public function generate( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): string; } Generator/PeclUuidRandomGenerator.php000064400000001447150251436620013737 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use function uuid_create; use function uuid_parse; use const UUID_TYPE_RANDOM; /** * PeclUuidRandomGenerator generates strings of random binary data using ext-uuid * * @link https://pecl.php.net/package/uuid ext-uuid */ class PeclUuidRandomGenerator implements RandomGeneratorInterface { public function generate(int $length): string { $uuid = uuid_create(UUID_TYPE_RANDOM); return uuid_parse($uuid); } } Generator/PeclUuidNameGenerator.php000064400000002510150251436620013367 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Exception\NameException; use Ramsey\Uuid\UuidInterface; use function sprintf; use function uuid_generate_md5; use function uuid_generate_sha1; use function uuid_parse; /** * PeclUuidNameGenerator generates strings of binary data from a namespace and a * name, using ext-uuid * * @link https://pecl.php.net/package/uuid ext-uuid */ class PeclUuidNameGenerator implements NameGeneratorInterface { /** @psalm-pure */ public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string { $uuid = match ($hashAlgorithm) { 'md5' => uuid_generate_md5($ns->toString(), $name), 'sha1' => uuid_generate_sha1($ns->toString(), $name), default => throw new NameException( sprintf( 'Unable to hash namespace and name with algorithm \'%s\'', $hashAlgorithm ) ), }; return uuid_parse($uuid); } } Generator/DefaultNameGenerator.php000064400000002363150251436620013247 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Exception\NameException; use Ramsey\Uuid\UuidInterface; use ValueError; use function hash; /** * DefaultNameGenerator generates strings of binary data based on a namespace, * name, and hashing algorithm */ class DefaultNameGenerator implements NameGeneratorInterface { /** @psalm-pure */ public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string { try { /** @var string|bool $bytes */ $bytes = @hash($hashAlgorithm, $ns->getBytes() . $name, true); } catch (ValueError $e) { $bytes = false; // keep same behavior than PHP 7 } if ($bytes === false) { throw new NameException(sprintf( 'Unable to hash namespace and name with algorithm \'%s\'', $hashAlgorithm )); } return (string) $bytes; } } Generator/CombGenerator.php000064400000006173150251436620011745 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use function bin2hex; use function explode; use function hex2bin; use function microtime; use function str_pad; use function substr; use const STR_PAD_LEFT; /** * CombGenerator generates COMBs (combined UUID/timestamp) * * The CombGenerator, when used with the StringCodec (and, by proxy, the * TimestampLastCombCodec) or the TimestampFirstCombCodec, combines the current * timestamp with a UUID (hence the name "COMB"). The timestamp either appears * as the first or last 48 bits of the COMB, depending on the codec used. * * By default, COMBs will have the timestamp set as the last 48 bits of the * identifier. * * ``` php * $factory = new UuidFactory(); * * $factory->setRandomGenerator(new CombGenerator( * $factory->getRandomGenerator(), * $factory->getNumberConverter() * )); * * $comb = $factory->uuid4(); * ``` * * To generate a COMB with the timestamp as the first 48 bits, set the * TimestampFirstCombCodec as the codec. * * ``` php * $factory->setCodec(new TimestampFirstCombCodec($factory->getUuidBuilder())); * ``` * * @link https://www.informit.com/articles/printerfriendly/25862 The Cost of GUIDs as Primary Keys */ class CombGenerator implements RandomGeneratorInterface { public const TIMESTAMP_BYTES = 6; public function __construct( private RandomGeneratorInterface $generator, private NumberConverterInterface $numberConverter ) { } /** * @throws InvalidArgumentException if $length is not a positive integer * greater than or equal to CombGenerator::TIMESTAMP_BYTES * * @inheritDoc */ public function generate(int $length): string { if ($length < self::TIMESTAMP_BYTES) { throw new InvalidArgumentException( 'Length must be a positive integer greater than or equal to ' . self::TIMESTAMP_BYTES ); } $hash = ''; if (self::TIMESTAMP_BYTES > 0 && $length > self::TIMESTAMP_BYTES) { $hash = $this->generator->generate($length - self::TIMESTAMP_BYTES); } $lsbTime = str_pad( $this->numberConverter->toHex($this->timestamp()), self::TIMESTAMP_BYTES * 2, '0', STR_PAD_LEFT ); return (string) hex2bin( str_pad( bin2hex($hash), $length - self::TIMESTAMP_BYTES, '0' ) . $lsbTime ); } /** * Returns current timestamp a string integer, precise to 0.00001 seconds */ private function timestamp(): string { $time = explode(' ', microtime(false)); return $time[1] . substr($time[0], 2, 5); } } Generator/TimeGeneratorFactory.php000064400000002211150251436620013300 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Provider\TimeProviderInterface; /** * TimeGeneratorFactory retrieves a default time generator, based on the * environment */ class TimeGeneratorFactory { public function __construct( private NodeProviderInterface $nodeProvider, private TimeConverterInterface $timeConverter, private TimeProviderInterface $timeProvider ) { } /** * Returns a default time generator, based on the current environment */ public function getGenerator(): TimeGeneratorInterface { return new DefaultTimeGenerator( $this->nodeProvider, $this->timeConverter, $this->timeProvider ); } } Generator/NameGeneratorFactory.php000064400000001272150251436620013270 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; /** * NameGeneratorFactory retrieves a default name generator, based on the * environment */ class NameGeneratorFactory { /** * Returns a default name generator, based on the current environment */ public function getGenerator(): NameGeneratorInterface { return new DefaultNameGenerator(); } } Generator/RandomLibAdapter.php000064400000003026150251436620012360 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use RandomLib\Factory; use RandomLib\Generator; /** * RandomLibAdapter generates strings of random binary data using the * paragonie/random-lib library * * @deprecated This class will be removed in 5.0.0. Use the default * RandomBytesGenerator or implement your own generator that implements * RandomGeneratorInterface. * * @link https://packagist.org/packages/paragonie/random-lib paragonie/random-lib */ class RandomLibAdapter implements RandomGeneratorInterface { private Generator $generator; /** * Constructs a RandomLibAdapter * * By default, if no Generator is passed in, this creates a high-strength * generator to use when generating random binary data. * * @param Generator|null $generator The generator to use when generating binary data */ public function __construct(?Generator $generator = null) { if ($generator === null) { $factory = new Factory(); $generator = $factory->getHighStrengthGenerator(); } $this->generator = $generator; } public function generate(int $length): string { return $this->generator->generate($length); } } Generator/PeclUuidTimeGenerator.php000064400000001551150251436620013411 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use function uuid_create; use function uuid_parse; use const UUID_TYPE_TIME; /** * PeclUuidTimeGenerator generates strings of binary data for time-base UUIDs, * using ext-uuid * * @link https://pecl.php.net/package/uuid ext-uuid */ class PeclUuidTimeGenerator implements TimeGeneratorInterface { /** * @inheritDoc */ public function generate($node = null, ?int $clockSeq = null): string { $uuid = uuid_create(UUID_TYPE_TIME); return uuid_parse($uuid); } } Generator/TimeGeneratorInterface.php000064400000002206150251436620013575 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Type\Hexadecimal; /** * A time generator generates strings of binary data based on a node ID, * clock sequence, and the current time */ interface TimeGeneratorInterface { /** * Generate a binary string from a node ID, clock sequence, and current time * * @param Hexadecimal|int|string|null $node A 48-bit number representing the * hardware address; this number may be represented as an integer or a * hexadecimal string * @param int|null $clockSeq A 14-bit number used to help avoid duplicates * that could arise when the clock is set backwards in time or if the * node ID changes * * @return string A binary string */ public function generate($node = null, ?int $clockSeq = null): string; } Generator/RandomGeneratorFactory.php000064400000001304150251436620013624 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; /** * RandomGeneratorFactory retrieves a default random generator, based on the * environment */ class RandomGeneratorFactory { /** * Returns a default random generator, based on the current environment */ public function getGenerator(): RandomGeneratorInterface { return new RandomBytesGenerator(); } } Generator/RandomBytesGenerator.php000064400000002170150251436620013305 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Exception\RandomSourceException; use Throwable; /** * RandomBytesGenerator generates strings of random binary data using the * built-in `random_bytes()` PHP function * * @link http://php.net/random_bytes random_bytes() */ class RandomBytesGenerator implements RandomGeneratorInterface { /** * @throws RandomSourceException if random_bytes() throws an exception/error * * @inheritDoc */ public function generate(int $length): string { try { return random_bytes($length); } catch (Throwable $exception) { throw new RandomSourceException( $exception->getMessage(), (int) $exception->getCode(), $exception ); } } } Generator/RandomGeneratorInterface.php000064400000001337150251436620014123 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; /** * A random generator generates strings of random binary data */ interface RandomGeneratorInterface { /** * Generates a string of randomized binary data * * @param int<1, max> $length The number of bytes of random binary data to generate * * @return string A binary string */ public function generate(int $length): string; } Generator/UnixTimeGenerator.php000064400000013521150251436620012622 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Brick\Math\BigInteger; use DateTimeImmutable; use DateTimeInterface; use Ramsey\Uuid\Type\Hexadecimal; use function hash; use function pack; use function str_pad; use function strlen; use function substr; use function substr_replace; use function unpack; use const PHP_INT_SIZE; use const STR_PAD_LEFT; /** * UnixTimeGenerator generates bytes that combine a 48-bit timestamp in * milliseconds since the Unix Epoch with 80 random bits * * Code and concepts within this class are borrowed from the symfony/uid package * and are used under the terms of the MIT license distributed with symfony/uid. * * symfony/uid is copyright (c) Fabien Potencier. * * @link https://symfony.com/components/Uid Symfony Uid component * @link https://github.com/symfony/uid/blob/4f9f537e57261519808a7ce1d941490736522bbc/UuidV7.php Symfony UuidV7 class * @link https://github.com/symfony/uid/blob/6.2/LICENSE MIT License */ class UnixTimeGenerator implements TimeGeneratorInterface { private static string $time = ''; private static ?string $seed = null; private static int $seedIndex = 0; /** @var int[] */ private static array $rand = []; /** @var int[] */ private static array $seedParts; public function __construct( private RandomGeneratorInterface $randomGenerator, private int $intSize = PHP_INT_SIZE ) { } /** * @param Hexadecimal|int|string|null $node Unused in this generator * @param int|null $clockSeq Unused in this generator * @param DateTimeInterface $dateTime A date-time instance to use when * generating bytes * * @inheritDoc */ public function generate($node = null, ?int $clockSeq = null, ?DateTimeInterface $dateTime = null): string { $time = ($dateTime ?? new DateTimeImmutable('now'))->format('Uv'); if ($time > self::$time || ($dateTime !== null && $time !== self::$time)) { $this->randomize($time); } else { $time = $this->increment(); } if ($this->intSize >= 8) { $time = substr(pack('J', (int) $time), -6); } else { $time = str_pad(BigInteger::of($time)->toBytes(false), 6, "\x00", STR_PAD_LEFT); } /** @var non-empty-string */ return $time . pack('n*', self::$rand[1], self::$rand[2], self::$rand[3], self::$rand[4], self::$rand[5]); } private function randomize(string $time): void { if (self::$seed === null) { $seed = $this->randomGenerator->generate(16); self::$seed = $seed; } else { $seed = $this->randomGenerator->generate(10); } /** @var int[] $rand */ $rand = unpack('n*', $seed); $rand[1] &= 0x03ff; self::$rand = $rand; self::$time = $time; } /** * Special thanks to Nicolas Grekas for sharing the following information: * * Within the same ms, we increment the rand part by a random 24-bit number. * * Instead of getting this number from random_bytes(), which is slow, we get * it by sha512-hashing self::$seed. This produces 64 bytes of entropy, * which we need to split in a list of 24-bit numbers. unpack() first splits * them into 16 x 32-bit numbers; we take the first byte of each of these * numbers to get 5 extra 24-bit numbers. Then, we consume those numbers * one-by-one and run this logic every 21 iterations. * * self::$rand holds the random part of the UUID, split into 5 x 16-bit * numbers for x86 portability. We increment this random part by the next * 24-bit number in the self::$seedParts list and decrement * self::$seedIndex. * * @link https://twitter.com/nicolasgrekas/status/1583356938825261061 Tweet from Nicolas Grekas */ private function increment(): string { if (self::$seedIndex === 0 && self::$seed !== null) { self::$seed = hash('sha512', self::$seed, true); /** @var int[] $s */ $s = unpack('l*', self::$seed); $s[] = ($s[1] >> 8 & 0xff0000) | ($s[2] >> 16 & 0xff00) | ($s[3] >> 24 & 0xff); $s[] = ($s[4] >> 8 & 0xff0000) | ($s[5] >> 16 & 0xff00) | ($s[6] >> 24 & 0xff); $s[] = ($s[7] >> 8 & 0xff0000) | ($s[8] >> 16 & 0xff00) | ($s[9] >> 24 & 0xff); $s[] = ($s[10] >> 8 & 0xff0000) | ($s[11] >> 16 & 0xff00) | ($s[12] >> 24 & 0xff); $s[] = ($s[13] >> 8 & 0xff0000) | ($s[14] >> 16 & 0xff00) | ($s[15] >> 24 & 0xff); self::$seedParts = $s; self::$seedIndex = 21; } self::$rand[5] = 0xffff & $carry = self::$rand[5] + (self::$seedParts[self::$seedIndex--] & 0xffffff); self::$rand[4] = 0xffff & $carry = self::$rand[4] + ($carry >> 16); self::$rand[3] = 0xffff & $carry = self::$rand[3] + ($carry >> 16); self::$rand[2] = 0xffff & $carry = self::$rand[2] + ($carry >> 16); self::$rand[1] += $carry >> 16; if (0xfc00 & self::$rand[1]) { $time = self::$time; $mtime = (int) substr($time, -9); if ($this->intSize >= 8 || strlen($time) < 10) { $time = (string) ((int) $time + 1); } elseif ($mtime === 999999999) { $time = (1 + (int) substr($time, 0, -9)) . '000000000'; } else { $mtime++; $time = substr_replace($time, str_pad((string) $mtime, 9, '0', STR_PAD_LEFT), -9); } $this->randomize($time); } return self::$time; } } Generator/NameGeneratorInterface.php000064400000002052150251436620013556 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\UuidInterface; /** * A name generator generates strings of binary data created by hashing together * a namespace with a name, according to a hashing algorithm */ interface NameGeneratorInterface { /** * Generate a binary string from a namespace and name hashed together with * the specified hashing algorithm * * @param UuidInterface $ns The namespace * @param string $name The name to use for creating a UUID * @param string $hashAlgorithm The hashing algorithm to use * * @return string A binary string * * @psalm-pure */ public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string; } Generator/DefaultTimeGenerator.php000064400000007463150251436620013273 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Exception\RandomSourceException; use Ramsey\Uuid\Exception\TimeSourceException; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\Provider\TimeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use Throwable; use function dechex; use function hex2bin; use function is_int; use function pack; use function preg_match; use function sprintf; use function str_pad; use function strlen; use const STR_PAD_LEFT; /** * DefaultTimeGenerator generates strings of binary data based on a node ID, * clock sequence, and the current time */ class DefaultTimeGenerator implements TimeGeneratorInterface { public function __construct( private NodeProviderInterface $nodeProvider, private TimeConverterInterface $timeConverter, private TimeProviderInterface $timeProvider ) { } /** * @throws InvalidArgumentException if the parameters contain invalid values * @throws RandomSourceException if random_int() throws an exception/error * * @inheritDoc */ public function generate($node = null, ?int $clockSeq = null): string { if ($node instanceof Hexadecimal) { $node = $node->toString(); } $node = $this->getValidNode($node); if ($clockSeq === null) { try { // This does not use "stable storage"; see RFC 4122, Section 4.2.1.1. $clockSeq = random_int(0, 0x3fff); } catch (Throwable $exception) { throw new RandomSourceException( $exception->getMessage(), (int) $exception->getCode(), $exception ); } } $time = $this->timeProvider->getTime(); $uuidTime = $this->timeConverter->calculateTime( $time->getSeconds()->toString(), $time->getMicroseconds()->toString() ); $timeHex = str_pad($uuidTime->toString(), 16, '0', STR_PAD_LEFT); if (strlen($timeHex) !== 16) { throw new TimeSourceException(sprintf( 'The generated time of \'%s\' is larger than expected', $timeHex )); } $timeBytes = (string) hex2bin($timeHex); return $timeBytes[4] . $timeBytes[5] . $timeBytes[6] . $timeBytes[7] . $timeBytes[2] . $timeBytes[3] . $timeBytes[0] . $timeBytes[1] . pack('n*', $clockSeq) . $node; } /** * Uses the node provider given when constructing this instance to get * the node ID (usually a MAC address) * * @param int|string|null $node A node value that may be used to override the node provider * * @return string 6-byte binary string representation of the node * * @throws InvalidArgumentException */ private function getValidNode(int | string | null $node): string { if ($node === null) { $node = $this->nodeProvider->getNode(); } // Convert the node to hex, if it is still an integer. if (is_int($node)) { $node = dechex($node); } if (!preg_match('/^[A-Fa-f0-9]+$/', (string) $node) || strlen((string) $node) > 12) { throw new InvalidArgumentException('Invalid node value'); } return (string) hex2bin(str_pad((string) $node, 12, '0', STR_PAD_LEFT)); } } Generator/DceSecurityGenerator.php000064400000010540150251436620013301 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Exception\DceSecurityException; use Ramsey\Uuid\Provider\DceSecurityProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Uuid; use function hex2bin; use function in_array; use function pack; use function str_pad; use function strlen; use function substr_replace; use const STR_PAD_LEFT; /** * DceSecurityGenerator generates strings of binary data based on a local * domain, local identifier, node ID, clock sequence, and the current time */ class DceSecurityGenerator implements DceSecurityGeneratorInterface { private const DOMAINS = [ Uuid::DCE_DOMAIN_PERSON, Uuid::DCE_DOMAIN_GROUP, Uuid::DCE_DOMAIN_ORG, ]; /** * Upper bounds for the clock sequence in DCE Security UUIDs. */ private const CLOCK_SEQ_HIGH = 63; /** * Lower bounds for the clock sequence in DCE Security UUIDs. */ private const CLOCK_SEQ_LOW = 0; public function __construct( private NumberConverterInterface $numberConverter, private TimeGeneratorInterface $timeGenerator, private DceSecurityProviderInterface $dceSecurityProvider ) { } public function generate( int $localDomain, ?IntegerObject $localIdentifier = null, ?Hexadecimal $node = null, ?int $clockSeq = null ): string { if (!in_array($localDomain, self::DOMAINS)) { throw new DceSecurityException( 'Local domain must be a valid DCE Security domain' ); } if ($localIdentifier && $localIdentifier->isNegative()) { throw new DceSecurityException( 'Local identifier out of bounds; it must be a value between 0 and 4294967295' ); } if ($clockSeq > self::CLOCK_SEQ_HIGH || $clockSeq < self::CLOCK_SEQ_LOW) { throw new DceSecurityException( 'Clock sequence out of bounds; it must be a value between 0 and 63' ); } switch ($localDomain) { case Uuid::DCE_DOMAIN_ORG: if ($localIdentifier === null) { throw new DceSecurityException( 'A local identifier must be provided for the org domain' ); } break; case Uuid::DCE_DOMAIN_PERSON: if ($localIdentifier === null) { $localIdentifier = $this->dceSecurityProvider->getUid(); } break; case Uuid::DCE_DOMAIN_GROUP: default: if ($localIdentifier === null) { $localIdentifier = $this->dceSecurityProvider->getGid(); } break; } $identifierHex = $this->numberConverter->toHex($localIdentifier->toString()); // The maximum value for the local identifier is 0xffffffff, or // 4294967295. This is 8 hexadecimal digits, so if the length of // hexadecimal digits is greater than 8, we know the value is greater // than 0xffffffff. if (strlen($identifierHex) > 8) { throw new DceSecurityException( 'Local identifier out of bounds; it must be a value between 0 and 4294967295' ); } $domainByte = pack('n', $localDomain)[1]; $identifierBytes = (string) hex2bin(str_pad($identifierHex, 8, '0', STR_PAD_LEFT)); if ($node instanceof Hexadecimal) { $node = $node->toString(); } // Shift the clock sequence 8 bits to the left, so it matches 0x3f00. if ($clockSeq !== null) { $clockSeq = $clockSeq << 8; } $bytes = $this->timeGenerator->generate($node, $clockSeq); // Replace bytes in the time-based UUID with DCE Security values. $bytes = substr_replace($bytes, $identifierBytes, 0, 4); return substr_replace($bytes, $domainByte, 9, 1); } } UuidInterface.php000064400000006011150251436620010006 0ustar00 * @license http://opensource.org/licenses/MIT MIT */ declare(strict_types=1); namespace Ramsey\Uuid; use JsonSerializable; use Ramsey\Uuid\Fields\FieldsInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; use Serializable; use Stringable; /** * A UUID is a universally unique identifier adhering to an agreed-upon * representation format and standard for generation * * @psalm-immutable */ interface UuidInterface extends DeprecatedUuidInterface, JsonSerializable, Serializable, Stringable { /** * Returns -1, 0, or 1 if the UUID is less than, equal to, or greater than * the other UUID * * The first of two UUIDs is greater than the second if the most * significant field in which the UUIDs differ is greater for the first * UUID. * * * Q. What's the value of being able to sort UUIDs? * * A. Use them as keys in a B-Tree or similar mapping. * * @param UuidInterface $other The UUID to compare * * @return int -1, 0, or 1 if the UUID is less than, equal to, or greater than $other */ public function compareTo(UuidInterface $other): int; /** * Returns true if the UUID is equal to the provided object * * The result is true if and only if the argument is not null, is a UUID * object, has the same variant, and contains the same value, bit for bit, * as the UUID. * * @param object|null $other An object to test for equality with this UUID * * @return bool True if the other object is equal to this UUID */ public function equals(?object $other): bool; /** * Returns the binary string representation of the UUID * * @psalm-return non-empty-string */ public function getBytes(): string; /** * Returns the fields that comprise this UUID */ public function getFields(): FieldsInterface; /** * Returns the hexadecimal representation of the UUID */ public function getHex(): Hexadecimal; /** * Returns the integer representation of the UUID */ public function getInteger(): IntegerObject; /** * Returns the string standard representation of the UUID as a URN * * @link http://en.wikipedia.org/wiki/Uniform_Resource_Name Uniform Resource Name * @link https://tools.ietf.org/html/rfc4122#section-3 RFC 4122, § 3: Namespace Registration Template */ public function getUrn(): string; /** * Returns the string standard representation of the UUID * * @psalm-return non-empty-string */ public function toString(): string; /** * Casts the UUID to the string standard representation * * @psalm-return non-empty-string */ public function __toString(): string; }