Commit version 24.12.13800
This commit is contained in:
786
libs/Brick/Math11/BigDecimal.php
Normal file
786
libs/Brick/Math11/BigDecimal.php
Normal file
@ -0,0 +1,786 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NegativeNumberException;
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Immutable, arbitrary-precision signed decimal numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigDecimal extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The unscaled value of this decimal number.
|
||||
*
|
||||
* This is a string of digits with an optional leading minus sign.
|
||||
* No leading zero must be present.
|
||||
* No leading minus sign must be present if the value is 0.
|
||||
*/
|
||||
private string $value;
|
||||
|
||||
/**
|
||||
* The scale (number of digits after the decimal point) of this decimal number.
|
||||
*
|
||||
* This must be zero or more.
|
||||
*/
|
||||
private int $scale;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param string $value The unscaled value, validated.
|
||||
* @param int $scale The scale, validated.
|
||||
*/
|
||||
protected function __construct(string $value, int $scale = 0)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->scale = $scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigDecimal of the given value.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
1079
libs/Brick/Math11/BigInteger.php
Normal file
1079
libs/Brick/Math11/BigInteger.php
Normal file
File diff suppressed because it is too large
Load Diff
512
libs/Brick/Math11/BigNumber.php
Normal file
512
libs/Brick/Math11/BigNumber.php
Normal file
@ -0,0 +1,512 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
|
||||
/**
|
||||
* Common interface for arbitrary-precision rational numbers.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class BigNumber implements \Serializable, \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The regular expression used to parse integer, decimal and rational numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?:' .
|
||||
'(?:' .
|
||||
'(?<integral>[0-9]+)?' .
|
||||
'(?<point>\.)?' .
|
||||
'(?<fractional>[0-9]+)?' .
|
||||
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||
')|(?:' .
|
||||
'(?<numerator>[0-9]+)' .
|
||||
'\/?' .
|
||||
'(?<denominator>[0-9]+)' .
|
||||
')' .
|
||||
')' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value.
|
||||
*
|
||||
* The concrete return type is dependent on the given value, with the following rules:
|
||||
*
|
||||
* - BigNumber instances are returned as is
|
||||
* - integer numbers are returned as BigInteger
|
||||
* - floating point numbers are converted to a string then parsed as such
|
||||
* - strings containing a `/` character are returned as BigRational
|
||||
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
445
libs/Brick/Math11/BigRational.php
Normal file
445
libs/Brick/Math11/BigRational.php
Normal file
@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
|
||||
/**
|
||||
* An arbitrarily large rational number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class BigRational extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The numerator.
|
||||
*/
|
||||
private BigInteger $numerator;
|
||||
|
||||
/**
|
||||
* The denominator. Always strictly positive.
|
||||
*/
|
||||
private BigInteger $denominator;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param BigInteger $numerator The numerator.
|
||||
* @param BigInteger $denominator The denominator.
|
||||
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||
*
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*/
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
|
||||
{
|
||||
if ($checkDenominator) {
|
||||
if ($denominator->isZero()) {
|
||||
throw DivisionByZeroException::denominatorMustNotBeZero();
|
||||
}
|
||||
|
||||
if ($denominator->isNegative()) {
|
||||
$numerator = $numerator->negated();
|
||||
$denominator = $denominator->negated();
|
||||
}
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigRational of the given value.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
35
libs/Brick/Math11/Exception/DivisionByZeroException.php
Normal file
35
libs/Brick/Math11/Exception/DivisionByZeroException.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
class DivisionByZeroException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function divisionByZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function modulusMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function denominatorMustNotBeZero() : DivisionByZeroException
|
||||
{
|
||||
return new self('The denominator of a rational number cannot be zero.');
|
||||
}
|
||||
}
|
23
libs/Brick/Math11/Exception/IntegerOverflowException.php
Normal file
23
libs/Brick/Math11/Exception/IntegerOverflowException.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
|
||||
/**
|
||||
* Exception thrown when an integer overflow occurs.
|
||||
*/
|
||||
class IntegerOverflowException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
|
||||
{
|
||||
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
|
||||
|
||||
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
}
|
12
libs/Brick/Math11/Exception/MathException.php
Normal file
12
libs/Brick/Math11/Exception/MathException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Base class for all math exceptions.
|
||||
*/
|
||||
class MathException extends \Exception
|
||||
{
|
||||
}
|
12
libs/Brick/Math11/Exception/NegativeNumberException.php
Normal file
12
libs/Brick/Math11/Exception/NegativeNumberException.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
|
||||
*/
|
||||
class NegativeNumberException extends MathException
|
||||
{
|
||||
}
|
33
libs/Brick/Math11/Exception/NumberFormatException.php
Normal file
33
libs/Brick/Math11/Exception/NumberFormatException.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||
*/
|
||||
class NumberFormatException extends MathException
|
||||
{
|
||||
/**
|
||||
* @param string $char The failing character.
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function charNotInAlphabet(string $char) : self
|
||||
{
|
||||
$ord = \ord($char);
|
||||
|
||||
if ($ord < 32 || $ord > 126) {
|
||||
$char = \strtoupper(\dechex($ord));
|
||||
|
||||
if ($ord < 10) {
|
||||
$char = '0' . $char;
|
||||
}
|
||||
} else {
|
||||
$char = '"' . $char . '"';
|
||||
}
|
||||
|
||||
return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
|
||||
}
|
||||
}
|
19
libs/Brick/Math11/Exception/RoundingNecessaryException.php
Normal file
19
libs/Brick/Math11/Exception/RoundingNecessaryException.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
class RoundingNecessaryException extends MathException
|
||||
{
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function roundingNecessary() : RoundingNecessaryException
|
||||
{
|
||||
return new self('Rounding is necessary to represent the result of the operation at this scale.');
|
||||
}
|
||||
}
|
676
libs/Brick/Math11/Internal/Calculator.php
Normal file
676
libs/Brick/Math11/Internal/Calculator.php
Normal file
@ -0,0 +1,676 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
/**
|
||||
* Performs basic operations on arbitrary size integers.
|
||||
*
|
||||
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
|
||||
* without leading zero, and with an optional leading minus sign if the number is not zero.
|
||||
*
|
||||
* Any other parameter format will lead to undefined behaviour.
|
||||
* All methods must return strings respecting this format, unless specified otherwise.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
abstract class Calculator
|
||||
{
|
||||
/**
|
||||
* The maximum exponent value allowed for the pow() method.
|
||||
*/
|
||||
public const MAX_POWER = 1000000;
|
||||
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* The Calculator instance in use.
|
||||
*/
|
||||
private static ?Calculator $instance = null;
|
||||
|
||||
/**
|
||||
* Sets the Calculator instance to use.
|
||||
*
|
||||
* An instance is typically set only in unit tests: the autodetect is usually the best option.
|
||||
*
|
||||
* @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
|
||||
*/
|
||||
final public static function set(?Calculator $calculator) : void
|
||||
{
|
||||
self::$instance = $calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Calculator instance to use.
|
||||
*
|
||||
* If none has been explicitly set, the fastest available implementation will be returned.
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-suppress ImpureStaticProperty
|
||||
*/
|
||||
final public static function get() : Calculator
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
/** @psalm-suppress ImpureMethodCall */
|
||||
self::$instance = self::detect();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fastest available Calculator implementation.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private static function detect() : Calculator
|
||||
{
|
||||
if (\extension_loaded('gmp')) {
|
||||
return new Calculator\GmpCalculator();
|
||||
}
|
||||
|
||||
if (\extension_loaded('bcmath')) {
|
||||
return new Calculator\BcMathCalculator();
|
||||
}
|
||||
|
||||
return new Calculator\NativeCalculator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the sign & digits of the operands.
|
||||
*
|
||||
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
|
||||
*/
|
||||
final protected function init(string $a, string $b) : array
|
||||
{
|
||||
return [
|
||||
$aNeg = ($a[0] === '-'),
|
||||
$bNeg = ($b[0] === '-'),
|
||||
|
||||
$aNeg ? \substr($a, 1) : $a,
|
||||
$bNeg ? \substr($b, 1) : $b,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of a number.
|
||||
*/
|
||||
final public function abs(string $n) : string
|
||||
{
|
||||
return ($n[0] === '-') ? \substr($n, 1) : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a number.
|
||||
*/
|
||||
final public function neg(string $n) : string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($n[0] === '-') {
|
||||
return \substr($n, 1);
|
||||
}
|
||||
|
||||
return '-' . $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
|
||||
*/
|
||||
final public function cmp(string $a, string $b) : int
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
if ($aNeg && ! $bNeg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($bNeg && ! $aNeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$aLen = \strlen($aDig);
|
||||
$bLen = \strlen($bDig);
|
||||
|
||||
if ($aLen < $bLen) {
|
||||
$result = -1;
|
||||
} elseif ($aLen > $bLen) {
|
||||
$result = 1;
|
||||
} else {
|
||||
$result = $aDig <=> $bDig;
|
||||
}
|
||||
|
||||
return $aNeg ? -$result : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two numbers.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
75
libs/Brick/Math11/Internal/Calculator/BcMathCalculator.php
Normal file
75
libs/Brick/Math11/Internal/Calculator/BcMathCalculator.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the bcmath library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class BcMathCalculator extends Calculator
|
||||
{
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \bcadd($a, $b, 0);
|
||||
}
|
||||
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \bcsub($a, $b, 0);
|
||||
}
|
||||
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \bcmul($a, $b, 0);
|
||||
}
|
||||
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \bcdiv($a, $b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
*/
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
return \bcmod($a, $b, 0);
|
||||
}
|
||||
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
$q = \bcdiv($a, $b, 0);
|
||||
$r = \bcmod($a, $b, 0);
|
||||
|
||||
assert($r !== null);
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \bcpow($a, (string) $e, 0);
|
||||
}
|
||||
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \bcpowmod($base, $exp, $mod, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* @psalm-suppress NullableReturnStatement
|
||||
*/
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \bcsqrt($n, 0);
|
||||
}
|
||||
}
|
108
libs/Brick/Math11/Internal/Calculator/GmpCalculator.php
Normal file
108
libs/Brick/Math11/Internal/Calculator/GmpCalculator.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the GMP library.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class GmpCalculator extends Calculator
|
||||
{
|
||||
public function add(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_add($a, $b));
|
||||
}
|
||||
|
||||
public function sub(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sub($a, $b));
|
||||
}
|
||||
|
||||
public function mul(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_mul($a, $b));
|
||||
}
|
||||
|
||||
public function divQ(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_q($a, $b));
|
||||
}
|
||||
|
||||
public function divR(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_div_r($a, $b));
|
||||
}
|
||||
|
||||
public function divQR(string $a, string $b) : array
|
||||
{
|
||||
[$q, $r] = \gmp_div_qr($a, $b);
|
||||
|
||||
return [
|
||||
\gmp_strval($q),
|
||||
\gmp_strval($r)
|
||||
];
|
||||
}
|
||||
|
||||
public function pow(string $a, int $e) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_pow($a, $e));
|
||||
}
|
||||
|
||||
public function modInverse(string $x, string $m) : ?string
|
||||
{
|
||||
$result = \gmp_invert($x, $m);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \gmp_strval($result);
|
||||
}
|
||||
|
||||
public function modPow(string $base, string $exp, string $mod) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_powm($base, $exp, $mod));
|
||||
}
|
||||
|
||||
public function gcd(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_gcd($a, $b));
|
||||
}
|
||||
|
||||
public function fromBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_init($number, $base));
|
||||
}
|
||||
|
||||
public function toBase(string $number, int $base) : string
|
||||
{
|
||||
return \gmp_strval($number, $base);
|
||||
}
|
||||
|
||||
public function and(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_and($a, $b));
|
||||
}
|
||||
|
||||
public function or(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_or($a, $b));
|
||||
}
|
||||
|
||||
public function xor(string $a, string $b) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_xor($a, $b));
|
||||
}
|
||||
|
||||
public function sqrt(string $n) : string
|
||||
{
|
||||
return \gmp_strval(\gmp_sqrt($n));
|
||||
}
|
||||
}
|
581
libs/Brick/Math11/Internal/Calculator/NativeCalculator.php
Normal file
581
libs/Brick/Math11/Internal/Calculator/NativeCalculator.php
Normal file
@ -0,0 +1,581 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
|
||||
/**
|
||||
* Calculator implementation using only native PHP code.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class NativeCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
|
||||
* For multiplication, this represents the max sum of the lengths of both operands.
|
||||
*
|
||||
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
|
||||
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
|
||||
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
|
||||
*/
|
||||
private int $maxDigits;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
switch (PHP_INT_SIZE) {
|
||||
case 4:
|
||||
$this->maxDigits = 9;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$this->maxDigits = 18;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
20
libs/Brick/Math11/LICENSE
Normal file
20
libs/Brick/Math11/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-present Benjamin Morel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
107
libs/Brick/Math11/RoundingMode.php
Normal file
107
libs/Brick/Math11/RoundingMode.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
/**
|
||||
* Specifies a rounding behavior for numerical operations capable of discarding precision.
|
||||
*
|
||||
* Each rounding mode indicates how the least significant returned digit of a rounded result
|
||||
* is to be calculated. If fewer digits are returned than the digits needed to represent the
|
||||
* exact numerical result, the discarded digits will be referred to as the discarded fraction
|
||||
* regardless the digits' contribution to the value of the number. In other words, considered
|
||||
* as a numerical value, the discarded fraction could have an absolute value greater than one.
|
||||
*/
|
||||
final class RoundingMode
|
||||
{
|
||||
/**
|
||||
* Private constructor. This class is not instantiable.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
|
||||
*
|
||||
* If this rounding mode is specified on an operation that yields a result that
|
||||
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
|
||||
*/
|
||||
public const UNNECESSARY = 0;
|
||||
|
||||
/**
|
||||
* Rounds away from zero.
|
||||
*
|
||||
* Always increments the digit prior to a nonzero discarded fraction.
|
||||
* Note that this rounding mode never decreases the magnitude of the calculated value.
|
||||
*/
|
||||
public const UP = 1;
|
||||
|
||||
/**
|
||||
* Rounds towards zero.
|
||||
*
|
||||
* Never increments the digit prior to a discarded fraction (i.e., truncates).
|
||||
* Note that this rounding mode never increases the magnitude of the calculated value.
|
||||
*/
|
||||
public const DOWN = 2;
|
||||
|
||||
/**
|
||||
* Rounds towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
|
||||
* Note that this rounding mode never decreases the calculated value.
|
||||
*/
|
||||
public const CEILING = 3;
|
||||
|
||||
/**
|
||||
* Rounds towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behave as for DOWN; if negative, behave as for UP.
|
||||
* Note that this rounding mode never increases the calculated value.
|
||||
*/
|
||||
public const FLOOR = 4;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
|
||||
* Note that this is the rounding mode commonly taught at school.
|
||||
*/
|
||||
public const HALF_UP = 5;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
|
||||
*
|
||||
* Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
|
||||
*/
|
||||
public const HALF_DOWN = 6;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
|
||||
*/
|
||||
public const HALF_CEILING = 7;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
|
||||
*/
|
||||
public const HALF_FLOOR = 8;
|
||||
|
||||
/**
|
||||
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
|
||||
*
|
||||
* Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
|
||||
* behaves as for HALF_DOWN if it's even.
|
||||
*
|
||||
* Note that this is the rounding mode that statistically minimizes
|
||||
* cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
|
||||
*/
|
||||
public const HALF_EVEN = 9;
|
||||
}
|
@ -5,11 +5,12 @@ declare(strict_types=1);
|
||||
namespace Doctrine\SqlFormatter;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
final class CliHighlighter implements Highlighter
|
||||
{
|
||||
const HIGHLIGHT_FUNCTIONS = 'functions';
|
||||
public const HIGHLIGHT_FUNCTIONS = 'functions';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private $escapeSequences;
|
||||
@ -33,9 +34,9 @@ final class CliHighlighter implements Highlighter
|
||||
];
|
||||
}
|
||||
|
||||
public function highlightToken(int $type, string $value) : string
|
||||
public function highlightToken(int $type, string $value): string
|
||||
{
|
||||
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value==='(' || $value===')')) {
|
||||
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value === '(' || $value === ')')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
@ -47,7 +48,7 @@ final class CliHighlighter implements Highlighter
|
||||
return $prefix . $value . "\x1b[0m";
|
||||
}
|
||||
|
||||
private function prefix(int $type)
|
||||
private function prefix(int $type): ?string
|
||||
{
|
||||
if (! isset(self::TOKEN_TYPE_TO_HIGHLIGHT[$type])) {
|
||||
return null;
|
||||
@ -56,7 +57,7 @@ final class CliHighlighter implements Highlighter
|
||||
return $this->escapeSequences[self::TOKEN_TYPE_TO_HIGHLIGHT[$type]];
|
||||
}
|
||||
|
||||
public function highlightError(string $value) : string
|
||||
public function highlightError(string $value): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s%s%s%s',
|
||||
@ -67,12 +68,12 @@ final class CliHighlighter implements Highlighter
|
||||
);
|
||||
}
|
||||
|
||||
public function highlightErrorMessage(string $value) : string
|
||||
public function highlightErrorMessage(string $value): string
|
||||
{
|
||||
return $this->highlightError($value);
|
||||
}
|
||||
|
||||
public function output(string $string) : string
|
||||
public function output(string $string): string
|
||||
{
|
||||
return $string . "\n";
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ final class Cursor
|
||||
$this->tokens = $tokens;
|
||||
}
|
||||
|
||||
public function next(int $exceptTokenType = null)
|
||||
public function next(?int $exceptTokenType = null): ?Token
|
||||
{
|
||||
while ($token = $this->tokens[++$this->position] ?? null) {
|
||||
if ($exceptTokenType !== null && $token->isOfType($exceptTokenType)) {
|
||||
@ -33,7 +33,7 @@ final class Cursor
|
||||
return null;
|
||||
}
|
||||
|
||||
public function previous(int $exceptTokenType = null)
|
||||
public function previous(?int $exceptTokenType = null): ?Token
|
||||
{
|
||||
while ($token = $this->tokens[--$this->position] ?? null) {
|
||||
if ($exceptTokenType !== null && $token->isOfType($exceptTokenType)) {
|
||||
@ -46,7 +46,7 @@ final class Cursor
|
||||
return null;
|
||||
}
|
||||
|
||||
public function subCursor() : self
|
||||
public function subCursor(): self
|
||||
{
|
||||
$cursor = new self($this->tokens);
|
||||
$cursor->position = $this->position;
|
||||
|
@ -6,7 +6,7 @@ namespace Doctrine\SqlFormatter;
|
||||
|
||||
interface Highlighter
|
||||
{
|
||||
const TOKEN_TYPE_TO_HIGHLIGHT = [
|
||||
public const TOKEN_TYPE_TO_HIGHLIGHT = [
|
||||
Token::TOKEN_TYPE_BOUNDARY => self::HIGHLIGHT_BOUNDARY,
|
||||
Token::TOKEN_TYPE_WORD => self::HIGHLIGHT_WORD,
|
||||
Token::TOKEN_TYPE_BACKTICK_QUOTE => self::HIGHLIGHT_BACKTICK_QUOTE,
|
||||
@ -20,30 +20,30 @@ interface Highlighter
|
||||
Token::TOKEN_TYPE_BLOCK_COMMENT => self::HIGHLIGHT_COMMENT,
|
||||
];
|
||||
|
||||
const HIGHLIGHT_BOUNDARY = 'boundary';
|
||||
const HIGHLIGHT_WORD = 'word';
|
||||
const HIGHLIGHT_BACKTICK_QUOTE = 'backtickQuote';
|
||||
const HIGHLIGHT_QUOTE = 'quote';
|
||||
const HIGHLIGHT_RESERVED = 'reserved';
|
||||
const HIGHLIGHT_NUMBER = 'number';
|
||||
const HIGHLIGHT_VARIABLE = 'variable';
|
||||
const HIGHLIGHT_COMMENT = 'comment';
|
||||
const HIGHLIGHT_ERROR = 'error';
|
||||
public const HIGHLIGHT_BOUNDARY = 'boundary';
|
||||
public const HIGHLIGHT_WORD = 'word';
|
||||
public const HIGHLIGHT_BACKTICK_QUOTE = 'backtickQuote';
|
||||
public const HIGHLIGHT_QUOTE = 'quote';
|
||||
public const HIGHLIGHT_RESERVED = 'reserved';
|
||||
public const HIGHLIGHT_NUMBER = 'number';
|
||||
public const HIGHLIGHT_VARIABLE = 'variable';
|
||||
public const HIGHLIGHT_COMMENT = 'comment';
|
||||
public const HIGHLIGHT_ERROR = 'error';
|
||||
|
||||
/**
|
||||
* Highlights a token depending on its type.
|
||||
*/
|
||||
public function highlightToken(int $type, string $value) : string;
|
||||
public function highlightToken(int $type, string $value): string;
|
||||
|
||||
/**
|
||||
* Highlights a token which causes an issue
|
||||
*/
|
||||
public function highlightError(string $value) : string;
|
||||
public function highlightError(string $value): string;
|
||||
|
||||
/**
|
||||
* Highlights an error message
|
||||
*/
|
||||
public function highlightErrorMessage(string $value) : string;
|
||||
public function highlightErrorMessage(string $value): string;
|
||||
|
||||
/**
|
||||
* Helper function for building string output
|
||||
@ -52,5 +52,5 @@ interface Highlighter
|
||||
*
|
||||
* @return string The quoted string
|
||||
*/
|
||||
public function output(string $string) : string;
|
||||
public function output(string $string): string;
|
||||
}
|
||||
|
@ -7,13 +7,14 @@ namespace Doctrine\SqlFormatter;
|
||||
use function htmlentities;
|
||||
use function sprintf;
|
||||
use function trim;
|
||||
|
||||
use const ENT_COMPAT;
|
||||
use const ENT_IGNORE;
|
||||
use const PHP_EOL;
|
||||
|
||||
final class HtmlHighlighter implements Highlighter
|
||||
{
|
||||
const HIGHLIGHT_PRE = 'pre';
|
||||
public const HIGHLIGHT_PRE = 'pre';
|
||||
|
||||
/**
|
||||
* This flag tells us if queries need to be enclosed in <pre> tags
|
||||
@ -45,11 +46,11 @@ final class HtmlHighlighter implements Highlighter
|
||||
$this->usePre = $usePre;
|
||||
}
|
||||
|
||||
public function highlightToken(int $type, string $value) : string
|
||||
public function highlightToken(int $type, string $value): string
|
||||
{
|
||||
$value = htmlentities($value, ENT_COMPAT | ENT_IGNORE, 'UTF-8');
|
||||
|
||||
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value==='(' || $value===')')) {
|
||||
if ($type === Token::TOKEN_TYPE_BOUNDARY && ($value === '(' || $value === ')')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
@ -61,7 +62,7 @@ final class HtmlHighlighter implements Highlighter
|
||||
return '<span ' . $attributes . '>' . $value . '</span>';
|
||||
}
|
||||
|
||||
public function attributes(int $type)
|
||||
public function attributes(int $type): ?string
|
||||
{
|
||||
if (! isset(self::TOKEN_TYPE_TO_HIGHLIGHT[$type])) {
|
||||
return null;
|
||||
@ -70,7 +71,7 @@ final class HtmlHighlighter implements Highlighter
|
||||
return $this->htmlAttributes[self::TOKEN_TYPE_TO_HIGHLIGHT[$type]];
|
||||
}
|
||||
|
||||
public function highlightError(string $value) : string
|
||||
public function highlightError(string $value): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s<span %s>%s</span>',
|
||||
@ -80,16 +81,16 @@ final class HtmlHighlighter implements Highlighter
|
||||
);
|
||||
}
|
||||
|
||||
public function highlightErrorMessage(string $value) : string
|
||||
public function highlightErrorMessage(string $value): string
|
||||
{
|
||||
return $this->highlightError($value);
|
||||
}
|
||||
|
||||
public function output(string $string) : string
|
||||
public function output(string $string): string
|
||||
{
|
||||
$string =trim($string);
|
||||
$string = trim($string);
|
||||
|
||||
// This is derp truncate for long list
|
||||
// Added by Observium Developers. IN list truncated for a long list
|
||||
$string = preg_replace('!(IN</span>\s*)(\()([^\)]+)(\))!', '$1$2<div class="text-truncate" onclick="revealHiddenOverflow(this)">$3</div>$4', $string);
|
||||
|
||||
if (! $this->usePre) {
|
||||
|
@ -6,22 +6,22 @@ namespace Doctrine\SqlFormatter;
|
||||
|
||||
final class NullHighlighter implements Highlighter
|
||||
{
|
||||
public function highlightToken(int $type, string $value) : string
|
||||
public function highlightToken(int $type, string $value): string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function highlightError(string $value) : string
|
||||
public function highlightError(string $value): string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function highlightErrorMessage(string $value) : string
|
||||
public function highlightErrorMessage(string $value): string
|
||||
{
|
||||
return ' ' . $value;
|
||||
}
|
||||
|
||||
public function output(string $string) : string
|
||||
public function output(string $string): string
|
||||
{
|
||||
return $string;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ use function str_repeat;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function trim;
|
||||
|
||||
use const PHP_SAPI;
|
||||
|
||||
final class SqlFormatter
|
||||
@ -33,7 +34,7 @@ final class SqlFormatter
|
||||
/** @var Tokenizer */
|
||||
private $tokenizer;
|
||||
|
||||
public function __construct(Highlighter $highlighter = null)
|
||||
public function __construct(?Highlighter $highlighter = null)
|
||||
{
|
||||
$this->tokenizer = new Tokenizer();
|
||||
$this->highlighter = $highlighter ?? (PHP_SAPI === 'cli' ? new CliHighlighter() : new HtmlHighlighter());
|
||||
@ -46,7 +47,7 @@ final class SqlFormatter
|
||||
*
|
||||
* @return string The SQL string with HTML styles and formatting wrapped in a <pre> tag
|
||||
*/
|
||||
public function format(string $string, string $indentString = ' ') : string
|
||||
public function format(string $string, string $indentString = ' '): string
|
||||
{
|
||||
// This variable will be populated with formatted html
|
||||
$return = '';
|
||||
@ -148,7 +149,7 @@ final class SqlFormatter
|
||||
// Allow up to 3 non-whitespace tokens inside inline parentheses
|
||||
$length = 0;
|
||||
$subCursor = $cursor->subCursor();
|
||||
for ($j=1; $j<=250; $j++) {
|
||||
for ($j = 1; $j <= 250; $j++) {
|
||||
// Reached end of string
|
||||
$next = $subCursor->next(Token::TOKEN_TYPE_WHITESPACE);
|
||||
if (! $next) {
|
||||
@ -164,17 +165,19 @@ final class SqlFormatter
|
||||
}
|
||||
|
||||
// Reached an invalid token for inline parentheses
|
||||
if ($next->value()===';' || $next->value()==='(') {
|
||||
if ($next->value() === ';' || $next->value() === '(') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reached an invalid token type for inline parentheses
|
||||
if ($next->isOfType(
|
||||
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
|
||||
Token::TOKEN_TYPE_RESERVED_NEWLINE,
|
||||
Token::TOKEN_TYPE_COMMENT,
|
||||
Token::TOKEN_TYPE_BLOCK_COMMENT
|
||||
)) {
|
||||
if (
|
||||
$next->isOfType(
|
||||
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
|
||||
Token::TOKEN_TYPE_RESERVED_NEWLINE,
|
||||
Token::TOKEN_TYPE_COMMENT,
|
||||
Token::TOKEN_TYPE_BLOCK_COMMENT
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -206,8 +209,8 @@ final class SqlFormatter
|
||||
$indentLevel--;
|
||||
|
||||
// Reset indent level
|
||||
while ($j=array_shift($indentTypes)) {
|
||||
if ($j!=='special') {
|
||||
while ($j = array_shift($indentTypes)) {
|
||||
if ($j !== 'special') {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -232,7 +235,7 @@ final class SqlFormatter
|
||||
|
||||
// If the last indent type was 'special', decrease the special indent for this round
|
||||
reset($indentTypes);
|
||||
if (current($indentTypes)==='special') {
|
||||
if (current($indentTypes) === 'special') {
|
||||
$indentLevel--;
|
||||
array_shift($indentTypes);
|
||||
}
|
||||
@ -256,9 +259,11 @@ final class SqlFormatter
|
||||
if ($token->value() === 'LIMIT' && ! $inlineParentheses) {
|
||||
$clauseLimit = true;
|
||||
}
|
||||
} elseif ($clauseLimit &&
|
||||
} elseif (
|
||||
$clauseLimit &&
|
||||
$token->value() !== ',' &&
|
||||
! $token->isOfType(Token::TOKEN_TYPE_NUMBER, Token::TOKEN_TYPE_WHITESPACE)) {
|
||||
! $token->isOfType(Token::TOKEN_TYPE_NUMBER, Token::TOKEN_TYPE_WHITESPACE)
|
||||
) {
|
||||
// Checks if we are out of the limit clause
|
||||
$clauseLimit = false;
|
||||
} elseif ($token->value() === ',' && ! $inlineParentheses) {
|
||||
@ -294,9 +299,11 @@ final class SqlFormatter
|
||||
}
|
||||
|
||||
// If the token shouldn't have a space before it
|
||||
if ($token->value() === '.' ||
|
||||
if (
|
||||
$token->value() === '.' ||
|
||||
$token->value() === ',' ||
|
||||
$token->value() === ';') {
|
||||
$token->value() === ';'
|
||||
) {
|
||||
$return = rtrim($return, ' ');
|
||||
}
|
||||
|
||||
@ -322,12 +329,14 @@ final class SqlFormatter
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($prev->isOfType(
|
||||
Token::TOKEN_TYPE_QUOTE,
|
||||
Token::TOKEN_TYPE_BACKTICK_QUOTE,
|
||||
Token::TOKEN_TYPE_WORD,
|
||||
Token::TOKEN_TYPE_NUMBER
|
||||
)) {
|
||||
if (
|
||||
$prev->isOfType(
|
||||
Token::TOKEN_TYPE_QUOTE,
|
||||
Token::TOKEN_TYPE_BACKTICK_QUOTE,
|
||||
Token::TOKEN_TYPE_WORD,
|
||||
Token::TOKEN_TYPE_NUMBER
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -355,7 +364,7 @@ final class SqlFormatter
|
||||
*
|
||||
* @return string The SQL string with HTML styles applied
|
||||
*/
|
||||
public function highlight(string $string) : string
|
||||
public function highlight(string $string): string
|
||||
{
|
||||
$cursor = $this->tokenizer->tokenize($string);
|
||||
|
||||
@ -378,7 +387,7 @@ final class SqlFormatter
|
||||
*
|
||||
* @return string The SQL string without comments
|
||||
*/
|
||||
public function compress(string $string) : string
|
||||
public function compress(string $string): string
|
||||
{
|
||||
$result = '';
|
||||
$cursor = $this->tokenizer->tokenize($string);
|
||||
@ -392,11 +401,13 @@ final class SqlFormatter
|
||||
|
||||
// Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN")
|
||||
|
||||
if ($token->isOfType(
|
||||
Token::TOKEN_TYPE_RESERVED,
|
||||
Token::TOKEN_TYPE_RESERVED_NEWLINE,
|
||||
Token::TOKEN_TYPE_RESERVED_TOPLEVEL
|
||||
)) {
|
||||
if (
|
||||
$token->isOfType(
|
||||
Token::TOKEN_TYPE_RESERVED,
|
||||
Token::TOKEN_TYPE_RESERVED_NEWLINE,
|
||||
Token::TOKEN_TYPE_RESERVED_TOPLEVEL
|
||||
)
|
||||
) {
|
||||
$newValue = preg_replace('/\s+/', ' ', $token->value());
|
||||
assert($newValue !== null);
|
||||
$token = $token->withValue($newValue);
|
||||
|
@ -10,23 +10,23 @@ use function strpos;
|
||||
final class Token
|
||||
{
|
||||
// Constants for token types
|
||||
const TOKEN_TYPE_WHITESPACE = 0;
|
||||
const TOKEN_TYPE_WORD = 1;
|
||||
const TOKEN_TYPE_QUOTE = 2;
|
||||
const TOKEN_TYPE_BACKTICK_QUOTE = 3;
|
||||
const TOKEN_TYPE_RESERVED = 4;
|
||||
const TOKEN_TYPE_RESERVED_TOPLEVEL = 5;
|
||||
const TOKEN_TYPE_RESERVED_NEWLINE = 6;
|
||||
const TOKEN_TYPE_BOUNDARY = 7;
|
||||
const TOKEN_TYPE_COMMENT = 8;
|
||||
const TOKEN_TYPE_BLOCK_COMMENT = 9;
|
||||
const TOKEN_TYPE_NUMBER = 10;
|
||||
const TOKEN_TYPE_ERROR = 11;
|
||||
const TOKEN_TYPE_VARIABLE = 12;
|
||||
public const TOKEN_TYPE_WHITESPACE = 0;
|
||||
public const TOKEN_TYPE_WORD = 1;
|
||||
public const TOKEN_TYPE_QUOTE = 2;
|
||||
public const TOKEN_TYPE_BACKTICK_QUOTE = 3;
|
||||
public const TOKEN_TYPE_RESERVED = 4;
|
||||
public const TOKEN_TYPE_RESERVED_TOPLEVEL = 5;
|
||||
public const TOKEN_TYPE_RESERVED_NEWLINE = 6;
|
||||
public const TOKEN_TYPE_BOUNDARY = 7;
|
||||
public const TOKEN_TYPE_COMMENT = 8;
|
||||
public const TOKEN_TYPE_BLOCK_COMMENT = 9;
|
||||
public const TOKEN_TYPE_NUMBER = 10;
|
||||
public const TOKEN_TYPE_ERROR = 11;
|
||||
public const TOKEN_TYPE_VARIABLE = 12;
|
||||
|
||||
// Constants for different components of a token
|
||||
const TOKEN_TYPE = 0;
|
||||
const TOKEN_VALUE = 1;
|
||||
public const TOKEN_TYPE = 0;
|
||||
public const TOKEN_VALUE = 1;
|
||||
|
||||
/** @var int */
|
||||
private $type;
|
||||
@ -40,29 +40,29 @@ final class Token
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function value() : string
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function type() : int
|
||||
public function type(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isOfType(int ...$types) : bool
|
||||
public function isOfType(int ...$types): bool
|
||||
{
|
||||
return in_array($this->type, $types, true);
|
||||
}
|
||||
|
||||
public function hasExtraWhitespace() : bool
|
||||
public function hasExtraWhitespace(): bool
|
||||
{
|
||||
return strpos($this->value(), ' ')!== false ||
|
||||
return strpos($this->value(), ' ') !== false ||
|
||||
strpos($this->value(), "\n") !== false ||
|
||||
strpos($this->value(), "\t") !== false;
|
||||
}
|
||||
|
||||
public function withValue(string $value) : self
|
||||
public function withValue(string $value): self
|
||||
{
|
||||
return new self($this->type(), $value);
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ final class Tokenizer
|
||||
'CONVERT',
|
||||
'CREATE',
|
||||
'CROSS',
|
||||
'CURRENT ROW',
|
||||
'CURRENT_TIMESTAMP',
|
||||
'DATABASE',
|
||||
'DATABASES',
|
||||
@ -108,11 +109,13 @@ final class Tokenizer
|
||||
'FAST',
|
||||
'FIELDS',
|
||||
'FILE',
|
||||
'FILTER',
|
||||
'FIRST',
|
||||
'FIXED',
|
||||
'FLUSH',
|
||||
'FOR',
|
||||
'FORCE',
|
||||
'FOLLOWING',
|
||||
'FOREIGN',
|
||||
'FULL',
|
||||
'FULLTEXT',
|
||||
@ -120,7 +123,8 @@ final class Tokenizer
|
||||
'GLOBAL',
|
||||
'GRANT',
|
||||
'GRANTS',
|
||||
'GROUP_CONCAT',
|
||||
'GROUP',
|
||||
'GROUPS',
|
||||
'HEAP',
|
||||
'HIGH_PRIORITY',
|
||||
'HOSTS',
|
||||
@ -180,6 +184,7 @@ final class Tokenizer
|
||||
'MYISAM',
|
||||
'NAMES',
|
||||
'NATURAL',
|
||||
'NO OTHERS',
|
||||
'NOT',
|
||||
'NOW()',
|
||||
'NULL',
|
||||
@ -192,12 +197,14 @@ final class Tokenizer
|
||||
'ON UPDATE',
|
||||
'ON DELETE',
|
||||
'OUTFILE',
|
||||
'OVER',
|
||||
'PACK_KEYS',
|
||||
'PAGE',
|
||||
'PARTIAL',
|
||||
'PARTITION',
|
||||
'PARTITIONS',
|
||||
'PASSWORD',
|
||||
'PRECEDING',
|
||||
'PRIMARY',
|
||||
'PRIVILEGES',
|
||||
'PROCEDURE',
|
||||
@ -213,6 +220,7 @@ final class Tokenizer
|
||||
'READ',
|
||||
'READ_ONLY',
|
||||
'READ_WRITE',
|
||||
'RECURSIVE',
|
||||
'REFERENCES',
|
||||
'REGEXP',
|
||||
'RELOAD',
|
||||
@ -277,6 +285,7 @@ final class Tokenizer
|
||||
'TEMPORARY',
|
||||
'TERMINATED',
|
||||
'THEN',
|
||||
'TIES',
|
||||
'TO',
|
||||
'TRAILING',
|
||||
'TRANSACTIONAL',
|
||||
@ -284,6 +293,7 @@ final class Tokenizer
|
||||
'TRUNCATE',
|
||||
'TYPE',
|
||||
'TYPES',
|
||||
'UNBOUNDED',
|
||||
'UNCOMMITTED',
|
||||
'UNIQUE',
|
||||
'UNLOCK',
|
||||
@ -307,6 +317,7 @@ final class Tokenizer
|
||||
* @var string[]
|
||||
*/
|
||||
private $reservedToplevel = [
|
||||
'WITH',
|
||||
'SELECT',
|
||||
'FROM',
|
||||
'WHERE',
|
||||
@ -327,6 +338,11 @@ final class Tokenizer
|
||||
'UNION',
|
||||
'EXCEPT',
|
||||
'INTERSECT',
|
||||
'PARTITION BY',
|
||||
'ROWS',
|
||||
'RANGE',
|
||||
'GROUPS',
|
||||
'WINDOW',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
@ -341,6 +357,7 @@ final class Tokenizer
|
||||
'XOR',
|
||||
'OR',
|
||||
'AND',
|
||||
'EXCLUDE',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
@ -351,6 +368,7 @@ final class Tokenizer
|
||||
'ADDTIME',
|
||||
'AES_DECRYPT',
|
||||
'AES_ENCRYPT',
|
||||
'APPROX_COUNT_DISTINCT',
|
||||
'AREA',
|
||||
'ASBINARY',
|
||||
'ASCII',
|
||||
@ -380,6 +398,7 @@ final class Tokenizer
|
||||
'CHARACTER_LENGTH',
|
||||
'CHARSET',
|
||||
'CHAR_LENGTH',
|
||||
'CHECKSUM_AGG',
|
||||
'COALESCE',
|
||||
'COERCIBILITY',
|
||||
'COLLATION',
|
||||
@ -395,8 +414,10 @@ final class Tokenizer
|
||||
'COS',
|
||||
'COT',
|
||||
'COUNT',
|
||||
'COUNT_BIG',
|
||||
'CRC32',
|
||||
'CROSSES',
|
||||
'CUME_DIST',
|
||||
'CURDATE',
|
||||
'CURRENT_DATE',
|
||||
'CURRENT_TIME',
|
||||
@ -418,6 +439,7 @@ final class Tokenizer
|
||||
'DECODE',
|
||||
'DEFAULT',
|
||||
'DEGREES',
|
||||
'DENSE_RANK',
|
||||
'DES_DECRYPT',
|
||||
'DES_ENCRYPT',
|
||||
'DIFFERENCE',
|
||||
@ -437,6 +459,7 @@ final class Tokenizer
|
||||
'EXTRACTVALUE',
|
||||
'FIELD',
|
||||
'FIND_IN_SET',
|
||||
'FIRST_VALUE',
|
||||
'FLOOR',
|
||||
'FORMAT',
|
||||
'FOUND_ROWS',
|
||||
@ -457,6 +480,8 @@ final class Tokenizer
|
||||
'GET_LOCK',
|
||||
'GLENGTH',
|
||||
'GREATEST',
|
||||
'GROUPING',
|
||||
'GROUPING_ID',
|
||||
'GROUP_CONCAT',
|
||||
'GROUP_UNIQUE_USERS',
|
||||
'HEX',
|
||||
@ -478,9 +503,12 @@ final class Tokenizer
|
||||
'ISSIMPLE',
|
||||
'IS_FREE_LOCK',
|
||||
'IS_USED_LOCK',
|
||||
'LAG',
|
||||
'LAST_DAY',
|
||||
'LAST_INSERT_ID',
|
||||
'LAST_VALUE',
|
||||
'LCASE',
|
||||
'LEAD',
|
||||
'LEAST',
|
||||
'LEFT',
|
||||
'LENGTH',
|
||||
@ -489,6 +517,7 @@ final class Tokenizer
|
||||
'LINESTRING',
|
||||
'LINESTRINGFROMTEXT',
|
||||
'LINESTRINGFROMWKB',
|
||||
'LISTAGG',
|
||||
'LN',
|
||||
'LOAD_FILE',
|
||||
'LOCALTIME',
|
||||
@ -536,6 +565,8 @@ final class Tokenizer
|
||||
'MULTIPOLYGONFROMTEXT',
|
||||
'MULTIPOLYGONFROMWKB',
|
||||
'NAME_CONST',
|
||||
'NTH_VALUE',
|
||||
'NTILE',
|
||||
'NULLIF',
|
||||
'NUMGEOMETRIES',
|
||||
'NUMINTERIORRINGS',
|
||||
@ -546,6 +577,9 @@ final class Tokenizer
|
||||
'ORD',
|
||||
'OVERLAPS',
|
||||
'PASSWORD',
|
||||
'PERCENT_RANK',
|
||||
'PERCENTILE_CONT',
|
||||
'PERCENTILE_DISC',
|
||||
'PERIOD_ADD',
|
||||
'PERIOD_DIFF',
|
||||
'PI',
|
||||
@ -566,6 +600,7 @@ final class Tokenizer
|
||||
'QUOTE',
|
||||
'RADIANS',
|
||||
'RAND',
|
||||
'RANK',
|
||||
'RELATED',
|
||||
'RELEASE_LOCK',
|
||||
'REPEAT',
|
||||
@ -574,6 +609,7 @@ final class Tokenizer
|
||||
'RIGHT',
|
||||
'ROUND',
|
||||
'ROW_COUNT',
|
||||
'ROW_NUMBER',
|
||||
'RPAD',
|
||||
'RTRIM',
|
||||
'SCHEMA',
|
||||
@ -591,9 +627,12 @@ final class Tokenizer
|
||||
'SRID',
|
||||
'STARTPOINT',
|
||||
'STD',
|
||||
'STDEV',
|
||||
'STDEVP',
|
||||
'STDDEV',
|
||||
'STDDEV_POP',
|
||||
'STDDEV_SAMP',
|
||||
'STRING_AGG',
|
||||
'STRCMP',
|
||||
'STR_TO_DATE',
|
||||
'SUBDATE',
|
||||
@ -630,7 +669,9 @@ final class Tokenizer
|
||||
'UTC_TIME',
|
||||
'UTC_TIMESTAMP',
|
||||
'UUID',
|
||||
'VAR',
|
||||
'VARIANCE',
|
||||
'VARP',
|
||||
'VAR_POP',
|
||||
'VAR_SAMP',
|
||||
'VERSION',
|
||||
@ -669,6 +710,7 @@ final class Tokenizer
|
||||
private $boundaries = [
|
||||
',',
|
||||
';',
|
||||
'::', // PostgreSQL cast operator
|
||||
':',
|
||||
')',
|
||||
'(',
|
||||
@ -727,7 +769,7 @@ final class Tokenizer
|
||||
*
|
||||
* @param string $string The SQL string
|
||||
*/
|
||||
public function tokenize(string $string) : Cursor
|
||||
public function tokenize(string $string): Cursor
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
@ -774,7 +816,7 @@ final class Tokenizer
|
||||
*
|
||||
* @return Token An associative array containing the type and value of the token.
|
||||
*/
|
||||
private function createNextToken(string $string, Token $previous = null) : Token
|
||||
private function createNextToken(string $string, ?Token $previous = null): Token
|
||||
{
|
||||
$matches = [];
|
||||
// Whitespace
|
||||
@ -783,9 +825,11 @@ final class Tokenizer
|
||||
}
|
||||
|
||||
// Comment
|
||||
if ($string[0] === '#' ||
|
||||
(isset($string[1]) && ($string[0]==='-' && $string[1]==='-') ||
|
||||
(isset($string[1]) && $string[0]==='/' && $string[1]==='*'))) {
|
||||
if (
|
||||
$string[0] === '#' ||
|
||||
(isset($string[1]) && (($string[0] === '-' && $string[1] === '-') ||
|
||||
($string[0] === '/' && $string[1] === '*')))
|
||||
) {
|
||||
// Comment until end of line
|
||||
if ($string[0] === '-' || $string[0] === '#') {
|
||||
$last = strpos($string, "\n");
|
||||
@ -805,9 +849,9 @@ final class Tokenizer
|
||||
}
|
||||
|
||||
// Quoted String
|
||||
if ($string[0]==='"' || $string[0]==='\'' || $string[0]==='`' || $string[0]==='[') {
|
||||
if ($string[0] === '"' || $string[0] === '\'' || $string[0] === '`' || $string[0] === '[') {
|
||||
return new Token(
|
||||
($string[0]==='`' || $string[0]==='['
|
||||
($string[0] === '`' || $string[0] === '['
|
||||
? Token::TOKEN_TYPE_BACKTICK_QUOTE
|
||||
: Token::TOKEN_TYPE_QUOTE),
|
||||
$this->getQuotedString($string)
|
||||
@ -820,7 +864,7 @@ final class Tokenizer
|
||||
$type = Token::TOKEN_TYPE_VARIABLE;
|
||||
|
||||
// If the variable name is quoted
|
||||
if ($string[1]==='"' || $string[1]==='\'' || $string[1]==='`') {
|
||||
if ($string[1] === '"' || $string[1] === '\'' || $string[1] === '`') {
|
||||
$value = $string[0] . $this->getQuotedString(substr($string, 1));
|
||||
} else {
|
||||
// Non-quoted variable name
|
||||
@ -836,11 +880,13 @@ final class Tokenizer
|
||||
}
|
||||
|
||||
// Number (decimal, binary, or hex)
|
||||
if (preg_match(
|
||||
'/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|' . $this->regexBoundaries . ')/',
|
||||
$string,
|
||||
$matches
|
||||
)) {
|
||||
if (
|
||||
preg_match(
|
||||
'/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|' . $this->regexBoundaries . ')/',
|
||||
$string,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
return new Token(Token::TOKEN_TYPE_NUMBER, $matches[1]);
|
||||
}
|
||||
|
||||
@ -854,38 +900,44 @@ final class Tokenizer
|
||||
if (! $previous || $previous->value() !== '.') {
|
||||
$upper = strtoupper($string);
|
||||
// Top Level Reserved Word
|
||||
if (preg_match(
|
||||
'/^(' . $this->regexReservedToplevel . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)) {
|
||||
if (
|
||||
preg_match(
|
||||
'/^(' . $this->regexReservedToplevel . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
return new Token(
|
||||
Token::TOKEN_TYPE_RESERVED_TOPLEVEL,
|
||||
substr($string, 0, strlen($matches[1]))
|
||||
substr($upper, 0, strlen($matches[1]))
|
||||
);
|
||||
}
|
||||
|
||||
// Newline Reserved Word
|
||||
if (preg_match(
|
||||
'/^(' . $this->regexReservedNewline . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)) {
|
||||
if (
|
||||
preg_match(
|
||||
'/^(' . $this->regexReservedNewline . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
return new Token(
|
||||
Token::TOKEN_TYPE_RESERVED_NEWLINE,
|
||||
substr($string, 0, strlen($matches[1]))
|
||||
substr($upper, 0, strlen($matches[1]))
|
||||
);
|
||||
}
|
||||
|
||||
// Other Reserved Word
|
||||
if (preg_match(
|
||||
'/^(' . $this->regexReserved . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)) {
|
||||
if (
|
||||
preg_match(
|
||||
'/^(' . $this->regexReserved . ')($|\s|' . $this->regexBoundaries . ')/',
|
||||
$upper,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
return new Token(
|
||||
Token::TOKEN_TYPE_RESERVED,
|
||||
substr($string, 0, strlen($matches[1]))
|
||||
substr($upper, 0, strlen($matches[1]))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -897,7 +949,7 @@ final class Tokenizer
|
||||
if (preg_match('/^(' . $this->regexFunction . '[(]|\s|[)])/', $upper, $matches)) {
|
||||
return new Token(
|
||||
Token::TOKEN_TYPE_RESERVED,
|
||||
substr($string, 0, strlen($matches[1])-1)
|
||||
substr($upper, 0, strlen($matches[1]) - 1)
|
||||
);
|
||||
}
|
||||
|
||||
@ -914,14 +966,14 @@ final class Tokenizer
|
||||
*
|
||||
* @return string[] The quoted strings
|
||||
*/
|
||||
private function quoteRegex(array $strings) : array
|
||||
private function quoteRegex(array $strings): array
|
||||
{
|
||||
return array_map(static function (string $string) : string {
|
||||
return array_map(static function (string $string): string {
|
||||
return preg_quote($string, '/');
|
||||
}, $strings);
|
||||
}
|
||||
|
||||
private function getQuotedString(string $string) : string
|
||||
private function getQuotedString(string $string): string
|
||||
{
|
||||
$ret = '';
|
||||
|
||||
@ -930,14 +982,16 @@ final class Tokenizer
|
||||
// 2. square bracket quoted string (SQL Server) using ]] to escape
|
||||
// 3. double quoted string using "" or \" to escape
|
||||
// 4. single quoted string using '' or \' to escape
|
||||
if (preg_match(
|
||||
'/^(((`[^`]*($|`))+)|
|
||||
if (
|
||||
preg_match(
|
||||
'/^(((`[^`]*($|`))+)|
|
||||
((\[[^\]]*($|\]))(\][^\]]*($|\]))*)|
|
||||
(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)|
|
||||
((\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*(\'|$))+))/sx',
|
||||
$string,
|
||||
$matches
|
||||
)) {
|
||||
$string,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
$ret = $matches[1];
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,15 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 0.7.0 (2017-05-23)
|
||||
|
||||
- [MAJOR]: [PR #46](https://github.com/fabiang/xmpp/pull/46) Added support for password-protected chatrooms
|
||||
- [MAJOR]: [PR #44](https://github.com/fabiang/xmpp/pull/44) Added anonymous authentication method
|
||||
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added support for registereing user
|
||||
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added vCard support
|
||||
- [MAJOR]: [PR #34](https://github.com/fabiang/xmpp/pull/34) Added support for blocking and unblocking an user
|
||||
- [MAJOR]: Drop support for PHP lower than 5.6
|
||||
- [MAJOR]: [PR #31](https://github.com/fabiang/xmpp/pull/31): Possibility to set context for SocketClient
|
||||
|
||||
## 0.6.1 (2014-11-20)
|
||||
|
||||
- [PATCH] [Issue #4](https://github.com/fabiang/xmpp/issues/4): Incomplete buffer response
|
||||
|
@ -84,7 +84,7 @@ abstract class AbstractConnection implements ConnectionInterface
|
||||
*
|
||||
* @var EventListenerInterface[]
|
||||
*/
|
||||
protected $listeners = array();
|
||||
protected $listeners = [];
|
||||
|
||||
/**
|
||||
* Connected.
|
||||
@ -259,7 +259,7 @@ abstract class AbstractConnection implements ConnectionInterface
|
||||
*/
|
||||
protected function log($message, $level = LogLevel::DEBUG)
|
||||
{
|
||||
$this->getEventManager()->trigger('logger', $this, array($message, $level));
|
||||
$this->getEventManager()->trigger('logger', $this, [$message, $level]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,7 @@ XML;
|
||||
*/
|
||||
public static function factory(Options $options)
|
||||
{
|
||||
$socket = new SocketClient($options->getAddress());
|
||||
$socket = new SocketClient($options->getAddress(), $options->getContextOptions());
|
||||
$object = new static($socket);
|
||||
$object->setOptions($options);
|
||||
return $object;
|
||||
@ -129,7 +129,7 @@ XML;
|
||||
// check if we didn't receive any data
|
||||
// if not we re-try to connect via TLS
|
||||
if (false === $this->receivedAnyData) {
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
$previousAddress = $this->getOptions()->getAddress();
|
||||
// only reconnect via tls if we've used tcp before.
|
||||
if (preg_match('#tcp://(?<address>.+)#', $previousAddress, $matches)) {
|
||||
|
@ -66,14 +66,14 @@ class Event implements EventInterface
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $parameters = array();
|
||||
protected $parameters = [];
|
||||
|
||||
/**
|
||||
* Event stack.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $eventStack = array();
|
||||
protected $eventStack = [];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
|
@ -55,7 +55,7 @@ class EventManager implements EventManagerInterface
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $events = array(self::WILDCARD => array());
|
||||
protected $events = [self::WILDCARD => []];
|
||||
|
||||
/**
|
||||
* Event object.
|
||||
@ -90,7 +90,7 @@ class EventManager implements EventManagerInterface
|
||||
}
|
||||
|
||||
if (!isset($this->events[$event])) {
|
||||
$this->events[$event] = array();
|
||||
$this->events[$event] = [];
|
||||
}
|
||||
|
||||
if (!in_array($callback, $this->events[$event], true)) {
|
||||
@ -107,13 +107,13 @@ class EventManager implements EventManagerInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$events = array();
|
||||
$events = [];
|
||||
if (!empty($this->events[$event])) {
|
||||
$events = $this->events[$event];
|
||||
}
|
||||
|
||||
$callbacks = array_merge($events, $this->events[self::WILDCARD]);
|
||||
$previous = array();
|
||||
$previous = [];
|
||||
|
||||
$eventObject = clone $this->getEventObject();
|
||||
$eventObject->setName($event);
|
||||
|
@ -69,6 +69,6 @@ class Logger extends AbstractEventListener
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
$this->getEventManager()->attach('logger', array($this, 'event'));
|
||||
$this->getEventManager()->attach('logger', [$this, 'event']);
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,8 @@ abstract class AbstractSessionEvent extends AbstractEventListener
|
||||
$this->blocking = true;
|
||||
$this->getConnection()->send(sprintf(
|
||||
$data,
|
||||
$this->getId()
|
||||
$this->getId(),
|
||||
$this->getOptions()->getResource()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class Authentication extends AbstractEventListener implements BlockingEventListe
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $mechanisms = array();
|
||||
protected $mechanisms = [];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@ -71,10 +71,10 @@ class Authentication extends AbstractEventListener implements BlockingEventListe
|
||||
public function attachEvents()
|
||||
{
|
||||
$input = $this->getConnection()->getInputStream()->getEventManager();
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms', array($this, 'authenticate'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism', array($this, 'collectMechanisms'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}failure', array($this, 'failure'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms', [$this, 'authenticate']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism', [$this, 'collectMechanisms']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}failure', [$this, 'failure']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', [$this, 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2016 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2016 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\EventListener\Stream\Authentication;
|
||||
|
||||
use Fabiang\Xmpp\EventListener\AbstractEventListener;
|
||||
|
||||
/**
|
||||
* Handler for "anonymous" authentication mechanism.
|
||||
*
|
||||
* @package Xmpp\EventListener\Authentication
|
||||
*/
|
||||
class Anonymous extends AbstractEventListener implements AuthenticationInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function authenticate($username, $password)
|
||||
{
|
||||
$this->getConnection()->send(
|
||||
'<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="ANONYMOUS"/>'
|
||||
);
|
||||
}
|
||||
}
|
@ -74,11 +74,11 @@ class DigestMd5 extends AbstractEventListener implements AuthenticationInterface
|
||||
public function attachEvents()
|
||||
{
|
||||
$input = $this->getInputEventManager();
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', array($this, 'challenge'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', [$this, 'challenge']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', [$this, 'success']);
|
||||
|
||||
$output = $this->getOutputEventManager();
|
||||
$output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', array($this, 'auth'));
|
||||
$output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', [$this, 'auth']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,10 +183,10 @@ class DigestMd5 extends AbstractEventListener implements AuthenticationInterface
|
||||
protected function parseCallenge($challenge)
|
||||
{
|
||||
if (!$challenge) {
|
||||
return array();
|
||||
return [];
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
preg_match_all('#(\w+)\=(?:"([^"]+)"|([^,]+))#', $challenge, $matches);
|
||||
list(, $variables, $quoted, $unquoted) = $matches;
|
||||
// filter empty strings; preserve keys
|
||||
|
@ -53,8 +53,8 @@ class Bind extends AbstractSessionEvent implements BlockingEventListenerInterfac
|
||||
public function attachEvents()
|
||||
{
|
||||
$input = $this->getInputEventManager();
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}bind', array($this, 'bindFeatures'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}jid', array($this, 'jid'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}bind', [$this, 'bindFeatures']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-bind}jid', [$this, 'jid']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +67,7 @@ class Bind extends AbstractSessionEvent implements BlockingEventListenerInterfac
|
||||
{
|
||||
$this->respondeToFeatures(
|
||||
$event,
|
||||
'<iq type="set" id="%s"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>'
|
||||
'<iq type="set" id="%s"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>%s</resource></bind></iq>'
|
||||
);
|
||||
}
|
||||
|
||||
|
144
libs/Fabiang/Xmpp/EventListener/Stream/BlockedUsers.php
Normal file
144
libs/Fabiang/Xmpp/EventListener/Stream/BlockedUsers.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\EventListener\Stream;
|
||||
|
||||
use Fabiang\Xmpp\Event\XMLEvent;
|
||||
use Fabiang\Xmpp\EventListener\AbstractEventListener;
|
||||
use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
|
||||
use Fabiang\Xmpp\Protocol\User\User;
|
||||
|
||||
/**
|
||||
* Listener
|
||||
*
|
||||
* @package Xmpp\EventListener
|
||||
*/
|
||||
class BlockedUsers extends AbstractEventListener implements BlockingEventListenerInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Blocking.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $blocking = false;
|
||||
|
||||
/**
|
||||
* user object.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $userObject;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
$this->getOutputEventManager()
|
||||
->attach('{urn:xmpp:blocking}blocklist', [$this, 'query']);
|
||||
$this->getInputEventManager()
|
||||
->attach('{urn:xmpp:blocking}blocklist', [$this, 'result']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending a query request for roster sets listener to blocking mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
$this->blocking = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result received.
|
||||
*
|
||||
* @param \Fabiang\Xmpp\Event\XMLEvent $event
|
||||
* @return void
|
||||
*/
|
||||
public function result(XMLEvent $event)
|
||||
{
|
||||
if ($event->isEndTag()) {
|
||||
$users = [];
|
||||
|
||||
/* @var $element \DOMElement */
|
||||
$element = $event->getParameter(0);
|
||||
$items = $element->getElementsByTagName('item');
|
||||
/* @var $item \DOMElement */
|
||||
foreach ($items as $item) {
|
||||
$users[] = $item->getAttribute('jid');
|
||||
}
|
||||
dd($users);
|
||||
//$this->getOptions()->setUsers($users);
|
||||
$this->blocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user object.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function getUserObject()
|
||||
{
|
||||
if (null === $this->userObject) {
|
||||
$this->setUserObject(new User);
|
||||
}
|
||||
|
||||
return $this->userObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user object.
|
||||
*
|
||||
* @param User $userObject
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserObject(User $userObject)
|
||||
{
|
||||
$this->userObject = $userObject;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isBlocking()
|
||||
{
|
||||
return $this->blocking;
|
||||
}
|
||||
}
|
111
libs/Fabiang/Xmpp/EventListener/Stream/Register.php
Normal file
111
libs/Fabiang/Xmpp/EventListener/Stream/Register.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\EventListener\Stream;
|
||||
|
||||
use Fabiang\Xmpp\Event\XMLEvent;
|
||||
use Fabiang\Xmpp\EventListener\AbstractEventListener;
|
||||
use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
|
||||
use Fabiang\Xmpp\Protocol\User\User;
|
||||
|
||||
/**
|
||||
* Listener
|
||||
*
|
||||
* @package Xmpp\EventListener
|
||||
*/
|
||||
class Register extends AbstractEventListener implements BlockingEventListenerInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Blocking.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $blocking = false;
|
||||
|
||||
/**
|
||||
* user object.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $userObject;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
$this->getOutputEventManager()
|
||||
->attach('{http://jabber.org/protocol/commands}command', [$this, 'query']);
|
||||
$this->getInputEventManager()
|
||||
->attach('{http://jabber.org/protocol/commands}command', [$this, 'result']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending a query request for roster sets listener to blocking mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
$this->blocking = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result received.
|
||||
*
|
||||
* @param \Fabiang\Xmpp\Event\XMLEvent $event
|
||||
* @return void
|
||||
*/
|
||||
public function result(XMLEvent $event)
|
||||
{
|
||||
if ($event->isEndTag()) {
|
||||
/* @var $element \DOMElement */
|
||||
$sid = $event->getParameter(0)->getAttribute('sessionid');
|
||||
|
||||
$this->getOptions()->setSid($sid);
|
||||
$this->blocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isBlocking()
|
||||
{
|
||||
return $this->blocking;
|
||||
}
|
||||
}
|
@ -69,9 +69,9 @@ class Roster extends AbstractEventListener implements BlockingEventListenerInter
|
||||
public function attachEvents()
|
||||
{
|
||||
$this->getOutputEventManager()
|
||||
->attach('{jabber:iq:roster}query', array($this, 'query'));
|
||||
->attach('{jabber:iq:roster}query', [$this, 'query']);
|
||||
$this->getInputEventManager()
|
||||
->attach('{jabber:iq:roster}query', array($this, 'result'));
|
||||
->attach('{jabber:iq:roster}query', [$this, 'result']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,7 +93,7 @@ class Roster extends AbstractEventListener implements BlockingEventListenerInter
|
||||
public function result(XMLEvent $event)
|
||||
{
|
||||
if ($event->isEndTag()) {
|
||||
$users = array();
|
||||
$users = [];
|
||||
|
||||
/* @var $element \DOMElement */
|
||||
$element = $event->getParameter(0);
|
||||
|
@ -53,8 +53,8 @@ class Session extends AbstractSessionEvent implements BlockingEventListenerInter
|
||||
public function attachEvents()
|
||||
{
|
||||
$input = $this->getInputEventManager();
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', array($this, 'sessionStart'));
|
||||
$input->attach('{jabber:client}iq', array($this, 'iq'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', [$this, 'sessionStart']);
|
||||
$input->attach('{jabber:client}iq', [$this, 'iq']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,8 +62,8 @@ class StartTls extends AbstractEventListener implements BlockingEventListenerInt
|
||||
public function attachEvents()
|
||||
{
|
||||
$input = $this->getInputEventManager();
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}starttls', array($this, 'starttlsEvent'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}proceed', array($this, 'proceed'));
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}starttls', [$this, 'starttlsEvent']);
|
||||
$input->attach('{urn:ietf:params:xml:ns:xmpp-tls}proceed', [$this, 'proceed']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,11 +61,11 @@ class Stream extends AbstractEventListener implements BlockingEventListenerInter
|
||||
public function attachEvents()
|
||||
{
|
||||
$this->getOutputEventManager()
|
||||
->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamStart'));
|
||||
->attach('{http://etherx.jabber.org/streams}stream', [$this, 'streamStart']);
|
||||
|
||||
$input = $this->getInputEventManager();
|
||||
$input->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamServer'));
|
||||
$input->attach('{http://etherx.jabber.org/streams}features', array($this, 'features'));
|
||||
$input->attach('{http://etherx.jabber.org/streams}stream', [$this, 'streamServer']);
|
||||
$input->attach('{http://etherx.jabber.org/streams}features', [$this, 'features']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,7 +55,7 @@ class StreamError extends AbstractEventListener
|
||||
{
|
||||
$this->getInputEventManager()->attach(
|
||||
'{http://etherx.jabber.org/streams}error',
|
||||
array($this, 'error')
|
||||
[$this, 'error']
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
Simplified BSD License
|
||||
======================
|
||||
|
||||
Copyright 2014 Fabian Grutschus.
|
||||
Copyright 2014-2017 Fabian Grutschus.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
@ -99,6 +99,12 @@ class Options
|
||||
*/
|
||||
protected $jid;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $sid;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var boolean
|
||||
@ -109,7 +115,7 @@ class Options
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $users = array();
|
||||
protected $users = [];
|
||||
|
||||
/**
|
||||
* Timeout for connection.
|
||||
@ -123,10 +129,20 @@ class Options
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $authenticationClasses = array(
|
||||
protected $authenticationClasses = [
|
||||
'digest-md5' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\DigestMd5',
|
||||
'plain' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Plain'
|
||||
);
|
||||
'plain' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Plain',
|
||||
'anonymous' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Anonymous'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Options used to create a stream context
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $contextOptions = [];
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@ -283,6 +299,18 @@ class Options
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
$username = $this->getUsername();
|
||||
$username = explode('/', $username);
|
||||
return isset($username[1]) ? $username[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get password.
|
||||
*
|
||||
@ -327,6 +355,28 @@ class Options
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users jid.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSid()
|
||||
{
|
||||
return $this->sid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set users jid.
|
||||
*
|
||||
* @param string $jid
|
||||
* @return $this
|
||||
*/
|
||||
public function setSid($sid)
|
||||
{
|
||||
$this->sid = (string) $sid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is user authenticated.
|
||||
*
|
||||
@ -413,4 +463,26 @@ class Options
|
||||
$this->timeout = (int) $timeout;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context options for connection
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getContextOptions()
|
||||
{
|
||||
return $this->contextOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set context options for connection
|
||||
*
|
||||
* @param array $contextOptions
|
||||
* @return \Fabiang\Xmpp\Options
|
||||
*/
|
||||
public function setContextOptions($contextOptions)
|
||||
{
|
||||
$this->contextOptions = (array) $contextOptions;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
113
libs/Fabiang/Xmpp/Protocol/BlockUser.php
Normal file
113
libs/Fabiang/Xmpp/Protocol/BlockUser.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\Protocol;
|
||||
|
||||
use Fabiang\Xmpp\Util\XML;
|
||||
|
||||
/**
|
||||
* Protocol setting for Xmpp.
|
||||
*
|
||||
* @package Xmpp\Protocol
|
||||
*/
|
||||
class BlockUser implements ProtocolImplementationInterface
|
||||
{
|
||||
protected $from;
|
||||
|
||||
// the jid of the user to block
|
||||
protected $accountjid;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return XML::quoteMessage(
|
||||
'<iq from="%s" type="set" id="%s">
|
||||
<block xmlns="urn:xmpp:blocking">
|
||||
<item jid="%s"/>
|
||||
</block>
|
||||
</iq>',
|
||||
$this->getFrom(),
|
||||
XML::generateId(),
|
||||
$this->getJabberID()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getJabberID()
|
||||
{
|
||||
return $this->accountjid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $accountjid
|
||||
* @return $this
|
||||
*/
|
||||
public function setJabberID($accountjid)
|
||||
{
|
||||
$this->accountjid = (string) $accountjid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFrom()
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setFrom($from)
|
||||
{
|
||||
$this->from = (string) $from;
|
||||
return $this;
|
||||
}
|
||||
}
|
57
libs/Fabiang/Xmpp/Protocol/BlockedUsers.php
Normal file
57
libs/Fabiang/Xmpp/Protocol/BlockedUsers.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\Protocol;
|
||||
|
||||
use Fabiang\Xmpp\Util\XML;
|
||||
|
||||
/**
|
||||
* Protocol setting for Xmpp.
|
||||
*
|
||||
* @package Xmpp\Protocol
|
||||
*/
|
||||
class BlockedUsers implements ProtocolImplementationInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return '<iq type="get" id="' . XML::generateId() . '">'
|
||||
. '<blocklist xmlns="urn:xmpp:blocking"/></iq>';
|
||||
}
|
||||
}
|
@ -47,6 +47,8 @@ use Fabiang\Xmpp\EventListener\Stream\Authentication;
|
||||
use Fabiang\Xmpp\EventListener\Stream\Bind;
|
||||
use Fabiang\Xmpp\EventListener\Stream\Session;
|
||||
use Fabiang\Xmpp\EventListener\Stream\Roster as RosterListener;
|
||||
use Fabiang\Xmpp\EventListener\Stream\Register as RegisterListener;
|
||||
use Fabiang\Xmpp\EventListener\Stream\BlockedUsers as BlockedUsersListener;
|
||||
|
||||
/**
|
||||
* Default Protocol implementation.
|
||||
@ -82,6 +84,8 @@ class DefaultImplementation implements ImplementationInterface
|
||||
$this->registerListener(new Bind);
|
||||
$this->registerListener(new Session);
|
||||
$this->registerListener(new RosterListener);
|
||||
$this->registerListener(new RegisterListener);
|
||||
$this->registerListener(new BlockedUsersListener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,6 +132,13 @@ class Presence implements ProtocolImplementationInterface
|
||||
*/
|
||||
protected $nickname;
|
||||
|
||||
/**
|
||||
* Channel password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -155,7 +162,14 @@ class Presence implements ProtocolImplementationInterface
|
||||
$presence .= ' to="' . XML::quote($this->getTo()) . '/' . XML::quote($this->getNickname()) . '"';
|
||||
}
|
||||
|
||||
return $presence . '><priority>' . $this->getPriority() . '</priority></presence>';
|
||||
$presence .= '><priority>' . $this->getPriority() . '</priority>';
|
||||
|
||||
if (null !== $this->getPassword()) {
|
||||
$presence .= "<x xmlns='http://jabber.org/protocol/muc'><password>" . $this->getPassword() . "</password></x>";
|
||||
}
|
||||
|
||||
$presence .= '</presence>';
|
||||
return $presence;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,4 +237,26 @@ class Presence implements ProtocolImplementationInterface
|
||||
$this->priority = (int) $priority;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel password.
|
||||
*
|
||||
* @return string¦null
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set channel password.
|
||||
*
|
||||
* @param string|null $to
|
||||
* @return $this
|
||||
*/
|
||||
public function setPassword($password = null)
|
||||
{
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
196
libs/Fabiang/Xmpp/Protocol/Register.php
Normal file
196
libs/Fabiang/Xmpp/Protocol/Register.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Fabiang\Xmpp\Protocol;
|
||||
|
||||
use Fabiang\Xmpp\Util\XML;
|
||||
|
||||
/**
|
||||
* Protocol setting for Xmpp.
|
||||
*
|
||||
* @package Xmpp\Protocol
|
||||
*/
|
||||
class Register implements ProtocolImplementationInterface
|
||||
{
|
||||
|
||||
protected $to;
|
||||
|
||||
protected $from;
|
||||
|
||||
protected $step;
|
||||
|
||||
protected $accountjid;
|
||||
|
||||
protected $password;
|
||||
|
||||
protected $sid;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param integer $priority
|
||||
* @param string $to
|
||||
* @param string $nickname
|
||||
*/
|
||||
public function __construct($to = null, $from = null, $step = 'one')
|
||||
{
|
||||
$this->setTo($to);
|
||||
$this->setFrom($from);
|
||||
$this->setStep($step);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
$req ='';
|
||||
|
||||
if($this->step == 'one')
|
||||
{
|
||||
$req = XML::quoteMessage(
|
||||
"<iq from='%s' id='%s' to='%s' type='set' xml:lang='en'>
|
||||
<command xmlns='http://jabber.org/protocol/commands' action='execute' node='http://jabber.org/protocol/admin#add-user'/>
|
||||
</iq>",
|
||||
$this->getFrom(),
|
||||
XML::generateId(),
|
||||
$this->getTo()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$req = XML::quoteMessage(
|
||||
"<iq from='%s' id='%s' to='%s' type='set' xml:lang='en'>
|
||||
<command xmlns='http://jabber.org/protocol/commands' node='http://jabber.org/protocol/admin#add-user' sessionid='%s'>
|
||||
<x xmlns='jabber:x:data' type='submit'>
|
||||
<field type='hidden' var='FORM_TYPE'>
|
||||
<value>http://jabber.org/protocol/admin</value>
|
||||
</field>
|
||||
<field var='accountjid'>
|
||||
<value>%s</value>
|
||||
</field>
|
||||
<field var='password'>
|
||||
<value>%s</value>
|
||||
</field>
|
||||
<field var='password-verify'>
|
||||
<value>%s</value>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>",
|
||||
$this->getFrom(),
|
||||
XML::generateId(),
|
||||
$this->getTo(),
|
||||
$this->getSID(),
|
||||
$this->getJabberID(),
|
||||
$this->getPassword(),
|
||||
$this->getPassword()
|
||||
);
|
||||
}
|
||||
|
||||
return $req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getJabberID()
|
||||
{
|
||||
return $this->accountjid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setJabberID($accountjid)
|
||||
{
|
||||
$this->accountjid = (string) $accountjid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTo()
|
||||
{
|
||||
return $this->to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setTo($to)
|
||||
{
|
||||
$this->to = (string) $to;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = (string) $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFrom()
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setFrom($from)
|
||||
{
|
||||
$this->from = (string) $from;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStep($step)
|
||||
{
|
||||
$this->step = (string) $step;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSID($sid)
|
||||
{
|
||||
$this->sid = (string) $sid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSID()
|
||||
{
|
||||
return $this->sid;
|
||||
}
|
||||
}
|
88
libs/Fabiang/Xmpp/Protocol/UnblockUser.php
Normal file
88
libs/Fabiang/Xmpp/Protocol/UnblockUser.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. 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.
|
||||
*
|
||||
* 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 OWNER OR 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.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those
|
||||
* of the authors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of the copyright holders.
|
||||
*
|
||||
* @author Fabian Grutschus <f.grutschus@lubyte.de>
|
||||
* @copyright 2014 Fabian Grutschus. All rights reserved.
|
||||
* @license BSD
|
||||
* @link http://github.com/fabiang/xmpp
|
||||
*/
|
||||
|
||||
namespace Fabiang\Xmpp\Protocol;
|
||||
|
||||
use Fabiang\Xmpp\Util\XML;
|
||||
|
||||
/**
|
||||
* Protocol setting for Xmpp.
|
||||
*
|
||||
* @package Xmpp\Protocol
|
||||
*/
|
||||
class UnblockUser implements ProtocolImplementationInterface
|
||||
{
|
||||
|
||||
// the jid of the user to block
|
||||
protected $accountjid;
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return XML::quoteMessage(
|
||||
'<iq type="set" id="%s">
|
||||
<unblock xmlns="urn:xmpp:blocking">
|
||||
<item jid="%s"/>
|
||||
</unblock>
|
||||
</iq>',
|
||||
XML::generateId(),
|
||||
$this->getJabberID()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getJabberID()
|
||||
{
|
||||
return $this->accountjid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setJabberID($accountjid)
|
||||
{
|
||||
$this->accountjid = (string) $accountjid;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ class User
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $groups = array();
|
||||
protected $groups = [];
|
||||
|
||||
public function getName()
|
||||
{
|
||||
|
228
libs/Fabiang/Xmpp/Protocol/VCard.php
Normal file
228
libs/Fabiang/Xmpp/Protocol/VCard.php
Normal file
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace Fabiang\Xmpp\Protocol;
|
||||
|
||||
use Fabiang\Xmpp\Util\XML;
|
||||
|
||||
/**
|
||||
* Protocol setting for Xmpp.
|
||||
*
|
||||
* @package Xmpp\Protocol
|
||||
*/
|
||||
class VCard implements ProtocolImplementationInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* vCard to.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $to;
|
||||
|
||||
|
||||
/**
|
||||
* user firstname.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $firstname;
|
||||
|
||||
/**
|
||||
* user lastname.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $lastname;
|
||||
|
||||
protected $jabberid;
|
||||
|
||||
protected $mime;
|
||||
|
||||
protected $image;
|
||||
|
||||
protected $ulr;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param integer $priority
|
||||
* @param string $to
|
||||
* @param string $nickname
|
||||
*/
|
||||
public function __construct($firstname = null, $lastname = null, $jabberid = null)
|
||||
{
|
||||
$this->setFirstname($firstname);
|
||||
$this->setLastname($lastname);
|
||||
$this->setJabberID($jabberid);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
|
||||
return XML::quoteMessage(
|
||||
'<iq id="' . XML::generateId() . '" type="set">
|
||||
<vCard xmlns="vcard-temp">
|
||||
<FN>%s</FN>
|
||||
<N>
|
||||
<FAMILY>%s</FAMILY>
|
||||
<GIVEN>%s</GIVEN>
|
||||
<MIDDLE/>
|
||||
</N>
|
||||
<NICKNAME>%s</NICKNAME>
|
||||
<URL>%s</URL>
|
||||
<PHOTO>
|
||||
<TYPE>%s</TYPE>
|
||||
<BINVAL>
|
||||
%s
|
||||
</BINVAL>
|
||||
</PHOTO>
|
||||
<JABBERID>%s</JABBERID>
|
||||
<DESC/>
|
||||
</vCard>
|
||||
</iq>',
|
||||
$this->getFirstname().' '.$this->getLastname(),
|
||||
$this->getLastname(),
|
||||
$this->getFirstname(),
|
||||
$this->getFirstname().' '.$this->getLastname(),
|
||||
$this->getUrl(),
|
||||
$this->getMime(),
|
||||
$this->getImage(),
|
||||
$this->getJabberID()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nickname.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstname()
|
||||
{
|
||||
return $this->firstname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nickname.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setFirstname($firstname)
|
||||
{
|
||||
$this->firstname = (string) $firstname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nickname.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLastname()
|
||||
{
|
||||
return $this->lastname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nickname.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setLastname($lastname)
|
||||
{
|
||||
$this->lastname = (string) $lastname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JabberID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getJabberID()
|
||||
{
|
||||
return $this->jabberid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set abberID.
|
||||
*
|
||||
* @param string $nickname
|
||||
* @return $this
|
||||
*/
|
||||
public function setJabberID($jabberid)
|
||||
{
|
||||
$this->jabberid = (string) $jabberid;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mime.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMime()
|
||||
{
|
||||
return $this->mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mime.
|
||||
*
|
||||
* @param string $mime
|
||||
* @return $this
|
||||
*/
|
||||
public function setMime($mime)
|
||||
{
|
||||
$this->mime = (string) $mime;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set image.
|
||||
*
|
||||
* @param string $image base64
|
||||
* @return $this
|
||||
*/
|
||||
public function setImage($image)
|
||||
{
|
||||
$this->image = (string) $image;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url.
|
||||
*
|
||||
* @param string $image base64
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrl($url)
|
||||
{
|
||||
$this->url = (string) $url;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -1,26 +1,28 @@
|
||||
# fabiang/xmpp
|
||||
|
||||
[](https://packagist.org/packages/fabiang/xmpp) [](https://packagist.org/packages/fabiang/xmpp) [](https://packagist.org/packages/fabiang/xmpp) [](https://packagist.org/packages/fabiang/xmpp)
|
||||
[](https://travis-ci.org/fabiang/xmpp) [](https://scrutinizer-ci.com/g/fabiang/xmpp/) [](https://coveralls.io/r/fabiang/xmpp?branch=master) [](https://gemnasium.com/fabiang/xmpp) [](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74)
|
||||
|
||||
Library for XMPP protocol connections (Jabber) for PHP.
|
||||
|
||||
[](https://packagist.org/packages/fabiang/xmpp)
|
||||
[](https://packagist.org/packages/fabiang/xmpp)
|
||||
[](https://packagist.org/packages/fabiang/xmpp)
|
||||
[](https://gemnasium.com/fabiang/xmpp)
|
||||
[](https://travis-ci.org/fabiang/xmpp)
|
||||
[](https://coveralls.io/r/fabiang/xmpp?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/fabiang/xmpp/)
|
||||
[](https://insight.sensiolabs.com/projects/a535cd82-788d-4506-803e-02ede44a9e74)
|
||||
|
||||
## SYSTEM REQUIREMENTS
|
||||
|
||||
- PHP >= 5.3.3
|
||||
- PHP minimum 5.6 or minimum 7.0
|
||||
- psr/log
|
||||
- psr/log-implementation - like monolog/monolog for logging (optional)
|
||||
- (optional) psr/log-implementation - like monolog/monolog for logging
|
||||
|
||||
## INSTALLATION
|
||||
|
||||
New to Composer? Read the [introduction](https://getcomposer.org/doc/00-intro.md#introduction). Add the following to your composer file:
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"fabiang/xmpp": "*"
|
||||
}
|
||||
}
|
||||
```bash
|
||||
composer require fabiang/xmpp
|
||||
```
|
||||
|
||||
## DOCUMENTATION
|
||||
@ -73,6 +75,7 @@ $client->send($message);
|
||||
// join a channel
|
||||
$channel = new Presence;
|
||||
$channel->setTo('channelname@conference.myjabber.com')
|
||||
->setPassword('channelpassword')
|
||||
->setNickName('mynick');
|
||||
$client->send($channel);
|
||||
|
||||
@ -95,14 +98,14 @@ $client->disconnect();
|
||||
If you like this library and you want to contribute, make sure the unit-tests and integration tests are running.
|
||||
Composer will help you to install the right version of PHPUnit and [Behat](http://behat.org/).
|
||||
|
||||
composer install --dev
|
||||
composer install
|
||||
|
||||
After that:
|
||||
|
||||
./vendor/bin/phpunit -c tests
|
||||
./vendor/bin/behat --config=tests/behat.yml --strict
|
||||
./vendor/bin/phpunit
|
||||
./vendor/bin/behat
|
||||
|
||||
New features should allways tested with Behat.
|
||||
New features should always tested with Behat.
|
||||
|
||||
## LICENSE
|
||||
|
||||
@ -112,5 +115,4 @@ BSD-2-Clause. See the [LICENSE](LICENSE.md).
|
||||
|
||||
- Better integration of channels
|
||||
- Factory method for server addresses
|
||||
- Add support von vCard
|
||||
- improve documentation
|
||||
|
@ -63,46 +63,57 @@ class SocketClient
|
||||
*/
|
||||
protected $address;
|
||||
|
||||
|
||||
/**
|
||||
* Options used to create a stream context
|
||||
* @see http://php.net/manual/en/function.stream-context-create.php
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Constructor takes address as argument.
|
||||
*
|
||||
* @param string $address
|
||||
*/
|
||||
public function __construct($address)
|
||||
public function __construct($address, $options = null)
|
||||
{
|
||||
$this->address = $address;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect.
|
||||
*
|
||||
* @param integer $timeout Timeout for connection
|
||||
* @param integer $timeout Timeout for connection
|
||||
* @param boolean $persistent Persitent connection
|
||||
* @return void
|
||||
*/
|
||||
public function connect($timeout = 30, $persistent = false)
|
||||
{
|
||||
$flags = STREAM_CLIENT_CONNECT;
|
||||
|
||||
if (true === $persistent) {
|
||||
$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
|
||||
} else {
|
||||
$flags = STREAM_CLIENT_CONNECT;
|
||||
$flags |= STREAM_CLIENT_PERSISTENT;
|
||||
}
|
||||
|
||||
// call stream_socket_client with custom error handler enabled
|
||||
$handler = new ErrorHandler(
|
||||
function ($address, $timeout, $flags) {
|
||||
$options = [
|
||||
'ssl' => [
|
||||
'allow_self_signed' => true,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($options);
|
||||
return stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context);
|
||||
function ($address, $timeout, $flags, array $options = null) {
|
||||
$errno = null;
|
||||
$errstr = null;
|
||||
|
||||
if (!empty($options)) {
|
||||
$context = stream_context_create($options);
|
||||
return stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context);
|
||||
}
|
||||
return stream_socket_client($address, $errno, $errstr, $timeout, $flags);
|
||||
},
|
||||
$this->address,
|
||||
$timeout,
|
||||
$flags
|
||||
$flags,
|
||||
$this->options
|
||||
);
|
||||
$resource = $handler->execute(__FILE__, __LINE__);
|
||||
|
||||
@ -113,9 +124,9 @@ class SocketClient
|
||||
/**
|
||||
* Reconnect and optionally use different address.
|
||||
*
|
||||
* @param string $address
|
||||
* @param string $address
|
||||
* @param integer $timeout
|
||||
* @param bool $persistent
|
||||
* @param bool $persistent
|
||||
*/
|
||||
public function reconnect($address = null, $timeout = 30, $persistent = false)
|
||||
{
|
||||
@ -146,7 +157,7 @@ class SocketClient
|
||||
*/
|
||||
public function setBlocking($flag = true)
|
||||
{
|
||||
stream_set_blocking($this->resource, (int) $flag);
|
||||
stream_set_blocking($this->resource, (int)$flag);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -164,7 +175,7 @@ class SocketClient
|
||||
/**
|
||||
* Write to stream.
|
||||
*
|
||||
* @param string $string String
|
||||
* @param string $string String
|
||||
* @param integer $length Limit
|
||||
* @return void
|
||||
*/
|
||||
@ -180,7 +191,7 @@ class SocketClient
|
||||
/**
|
||||
* Enable/disable cryptography on stream.
|
||||
*
|
||||
* @param boolean $enable Flag
|
||||
* @param boolean $enable Flag
|
||||
* @param integer $cryptoType One of the STREAM_CRYPTO_METHOD_* constants.
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
|
@ -85,21 +85,21 @@ class XMLStream implements EventManagerAwareInterface
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $namespaces = array();
|
||||
protected $namespaces = [];
|
||||
|
||||
/**
|
||||
* Cache of namespace prefixes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $namespacePrefixes = array();
|
||||
protected $namespacePrefixes = [];
|
||||
|
||||
/**
|
||||
* Element cache.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $elements = array();
|
||||
protected $elements = [];
|
||||
|
||||
/**
|
||||
* XML parser.
|
||||
@ -120,7 +120,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $eventCache = array();
|
||||
protected $eventCache = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@ -155,13 +155,13 @@ class XMLStream implements EventManagerAwareInterface
|
||||
{
|
||||
$this->clearDocument($source);
|
||||
|
||||
$this->eventCache = array();
|
||||
$this->eventCache = [];
|
||||
if (0 === xml_parse($this->parser, $source, false)) {
|
||||
throw XMLParserException::create($this->parser);
|
||||
}
|
||||
// trigger collected events.
|
||||
$this->trigger();
|
||||
$this->eventCache = array();
|
||||
$this->eventCache = [];
|
||||
|
||||
// </stream> was not there, so lets close the document
|
||||
if ($this->depth > 0) {
|
||||
@ -186,7 +186,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
if ('<?xml' === substr($source, 0, 5)) {
|
||||
$this->reset();
|
||||
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
if (preg_match('/^<\?xml.*encoding=(\'|")([\w-]+)\1.*?>/i', $source, $matches)) {
|
||||
$this->encoding = $matches[2];
|
||||
xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->encoding);
|
||||
@ -260,7 +260,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
$this->depth++;
|
||||
|
||||
$event = '{' . $namespaceElement . '}' . $elementName;
|
||||
$this->cacheEvent($event, true, array($element));
|
||||
$this->cacheEvent($event, true, [$element]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,7 +271,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
*/
|
||||
protected function createAttributeNodes(array $attribs)
|
||||
{
|
||||
$attributesNodes = array();
|
||||
$attributesNodes = [];
|
||||
foreach ($attribs as $name => $value) {
|
||||
// collect namespace prefixes
|
||||
if ('xmlns:' === substr($name, 0, 6)) {
|
||||
@ -316,7 +316,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
}
|
||||
|
||||
$event = '{' . $namespaceURI . '}' . $localName;
|
||||
$this->cacheEvent($event, false, array($element));
|
||||
$this->cacheEvent($event, false, [$element]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -345,7 +345,7 @@ class XMLStream implements EventManagerAwareInterface
|
||||
*/
|
||||
protected function cacheEvent($event, $startTag, $params)
|
||||
{
|
||||
$this->eventCache[] = array($event, $startTag, $params);
|
||||
$this->eventCache[] = [$event, $startTag, $params];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -382,9 +382,9 @@ class XMLStream implements EventManagerAwareInterface
|
||||
$this->parser = $parser;
|
||||
$this->depth = 0;
|
||||
$this->document = new \DOMDocument('1.0', $this->encoding);
|
||||
$this->namespaces = array();
|
||||
$this->namespacePrefixes = array();
|
||||
$this->elements = array();
|
||||
$this->namespaces = [];
|
||||
$this->namespacePrefixes = [];
|
||||
$this->elements = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +59,7 @@ class ErrorHandler
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $arguments = array();
|
||||
protected $arguments = [];
|
||||
|
||||
public function __construct($method)
|
||||
{
|
||||
|
@ -18,7 +18,7 @@
|
||||
* @author Nick Ilyin <nick.ilyin@gmail.com>
|
||||
* @author: Victor Stanciu <vic.stanciu@gmail.com> (original author)
|
||||
*
|
||||
* @version 2.8.41
|
||||
* @version 2.8.45
|
||||
*
|
||||
* Auto-generated isXXXX() magic methods.
|
||||
* php -a examples/dump_magic_methods.php
|
||||
@ -255,7 +255,7 @@ class Mobile_Detect
|
||||
/**
|
||||
* Stores the version number of the current release.
|
||||
*/
|
||||
const VERSION = '2.8.41';
|
||||
const VERSION = '2.8.45';
|
||||
|
||||
/**
|
||||
* A type for the version() method indicating a string return value.
|
||||
@ -416,7 +416,7 @@ class Mobile_Detect
|
||||
'NexusTablet' => 'Android.*Nexus[\s]+(7|9|10)',
|
||||
// https://en.wikipedia.org/wiki/Pixel_C
|
||||
'GoogleTablet' => 'Android.*Pixel C',
|
||||
'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone.
|
||||
'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5105|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925|GT-I9200|GT-P5200|GT-P5210|GT-P5210X|SM-T311|SM-T310|SM-T310X|SM-T210|SM-T210R|SM-T211|SM-P600|SM-P601|SM-P605|SM-P900|SM-P901|SM-T217|SM-T217A|SM-T217S|SM-P6000|SM-T3100|SGH-I467|XE500|SM-T110|GT-P5220|GT-I9200X|GT-N5110X|GT-N5120|SM-P905|SM-T111|SM-T2105|SM-T315|SM-T320|SM-T320X|SM-T321|SM-T520|SM-T525|SM-T530NU|SM-T230NU|SM-T330NU|SM-T900|XE500T1C|SM-P605V|SM-P905V|SM-T337V|SM-T537V|SM-T707V|SM-T807V|SM-P600X|SM-P900X|SM-T210X|SM-T230|SM-T230X|SM-T325|GT-P7503|SM-T531|SM-T330|SM-T530|SM-T705|SM-T705C|SM-T535|SM-T331|SM-T800|SM-T700|SM-T537|SM-T807|SM-P907A|SM-T337A|SM-T537A|SM-T707A|SM-T807A|SM-T237|SM-T807P|SM-P607T|SM-T217T|SM-T337T|SM-T807T|SM-T116NQ|SM-T116BU|SM-P550|SM-T350|SM-T550|SM-T9000|SM-P9000|SM-T705Y|SM-T805|GT-P3113|SM-T710|SM-T810|SM-T815|SM-T360|SM-T533|SM-T113|SM-T335|SM-T715|SM-T560|SM-T670|SM-T677|SM-T377|SM-T567|SM-T357T|SM-T555|SM-T561|SM-T713|SM-T719|SM-T813|SM-T819|SM-T580|SM-T355Y?|SM-T280|SM-T817A|SM-T820|SM-W700|SM-P580|SM-T587|SM-P350|SM-P555M|SM-P355M|SM-T113NU|SM-T815Y|SM-T585|SM-T285|SM-T825|SM-W708|SM-T835|SM-T830|SM-T837V|SM-T720|SM-T510|SM-T387V|SM-P610|SM-T290|SM-T515|SM-T590|SM-T595|SM-T725|SM-T817P|SM-P585N0|SM-T395|SM-T295|SM-T865|SM-P610N|SM-P615|SM-T970|SM-T380|SM-T5950|SM-T905|SM-T231|SM-T500|SM-T860|SM-T536|SM-T837A|SM-X200|SM-T220|SM-T870|SM-X906C|SM-X700|SM-X706|SM-X706B|SM-X706U|SM-X706N|SM-X800|SM-X806|SM-X806B|SM-X806U|SM-X806N|SM-X900|SM-X906|SM-X906B|SM-X906U|SM-X906N|SM-P613', // SCH-P709|SCH-P729|SM-T2558|GT-I9205 - Samsung Mega - treat them like a regular phone.
|
||||
// http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
|
||||
'Kindle' => 'Kindle|Silk.*Accelerated|Android.*\b(KFOT|KFTT|KFJWI|KFJWA|KFOTE|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|WFJWAE|KFSAWA|KFSAWI|KFASWI|KFARWI|KFFOWI|KFGIWI|KFMEWI)\b|Android.*Silk/[0-9.]+ like Chrome/[0-9.]+ (?!Mobile)',
|
||||
// Only the Surface tablets with Windows RT are considered mobile.
|
||||
@ -769,7 +769,7 @@ class Mobile_Detect
|
||||
// http://scottcate.com/technology/windows-phone-8-ie10-desktop-or-mobile/
|
||||
// https://github.com/serbanghita/Mobile-Detect/issues/57#issuecomment-15024011
|
||||
// https://developers.facebook.com/docs/sharing/webmasters/crawler/
|
||||
'Bot' => 'Googlebot|facebookexternalhit|Google-AMPHTML|s~amp-validator|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom|contentkingapp|AspiegelBot',
|
||||
'Bot' => 'Googlebot|facebookexternalhit|Google-AMPHTML|s~amp-validator|AdsBot-Google|Google Keyword Suggestion|Facebot|YandexBot|YandexMobileBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|Exabot|MJ12bot|YandexImages|TurnitinBot|Pingdom|contentkingapp|AspiegelBot|Semrush|DotBot|PetalBot|MetadataScraper',
|
||||
'MobileBot' => 'Googlebot-Mobile|AdsBot-Google-Mobile|YahooSeeker/M1A1-R2D2',
|
||||
'DesktopMode' => 'WPDesktop',
|
||||
'TV' => 'SonyDTV|HbbTV', // experimental
|
||||
@ -1180,10 +1180,10 @@ class Mobile_Detect
|
||||
|
||||
if (!$rules) {
|
||||
$rules = array_merge(
|
||||
self::$phoneDevices,
|
||||
self::$tabletDevices,
|
||||
self::$operatingSystems,
|
||||
self::$browsers
|
||||
self::getPhoneDevices(),
|
||||
self::getTabletDevices(),
|
||||
self::getOperatingSystems(),
|
||||
self::getBrowsers()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1208,11 +1208,11 @@ class Mobile_Detect
|
||||
if (!$rules) {
|
||||
// Merge all rules together.
|
||||
$rules = array_merge(
|
||||
self::$phoneDevices,
|
||||
self::$tabletDevices,
|
||||
self::$operatingSystems,
|
||||
self::$browsers,
|
||||
self::$utilities
|
||||
static::getPhoneDevices(),
|
||||
static::getTabletDevices(),
|
||||
static::getOperatingSystems(),
|
||||
static::getBrowsers(),
|
||||
static::getUtilities()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1406,7 +1406,7 @@ class Mobile_Detect
|
||||
|
||||
$this->setDetectionType(self::DETECTION_TYPE_MOBILE);
|
||||
|
||||
foreach (self::$tabletDevices as $_regex) {
|
||||
foreach (static::getTabletDevices() as $_regex) {
|
||||
if ($this->match($_regex, $userAgent)) {
|
||||
return true;
|
||||
}
|
||||
@ -1462,7 +1462,7 @@ class Mobile_Detect
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : $this->userAgent), $matches);
|
||||
$match = (bool) preg_match(sprintf('#%s#is', $regex), (false === empty($userAgent) ? $userAgent : (is_string($this->userAgent) ? $this->userAgent : '')), $matches);
|
||||
// If positive match is found, store the results for debug.
|
||||
if ($match) {
|
||||
$this->matchingRegex = $regex;
|
||||
@ -1531,7 +1531,7 @@ class Mobile_Detect
|
||||
$type = self::VERSION_TYPE_STRING;
|
||||
}
|
||||
|
||||
$properties = self::getProperties();
|
||||
$properties = static::getProperties();
|
||||
|
||||
// Check if the property exists in the properties array.
|
||||
if (true === isset($properties[$propertyName])) {
|
||||
@ -1545,7 +1545,7 @@ class Mobile_Detect
|
||||
$propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString);
|
||||
|
||||
// Identify and extract the version.
|
||||
preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match);
|
||||
preg_match(sprintf('#%s#is', $propertyPattern), (is_string($this->userAgent) ? $this->userAgent : ''), $match);
|
||||
|
||||
if (false === empty($match[1])) {
|
||||
$version = ($type == self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]);
|
||||
|
160
libs/Nette/Bridges/Nette/Bridge.php
Normal file
160
libs/Nette/Bridges/Nette/Bridge.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Bridges\Nette;
|
||||
|
||||
use Latte;
|
||||
use Nette;
|
||||
use Tracy;
|
||||
use Tracy\BlueScreen;
|
||||
use Tracy\Helpers;
|
||||
|
||||
|
||||
/**
|
||||
* Bridge for NEON & Latte.
|
||||
*/
|
||||
class Bridge
|
||||
{
|
||||
public static function initialize(): void
|
||||
{
|
||||
$blueScreen = Tracy\Debugger::getBlueScreen();
|
||||
if (!class_exists(Latte\Bridges\Tracy\BlueScreenPanel::class)) {
|
||||
$blueScreen->addPanel([self::class, 'renderLatteError']);
|
||||
$blueScreen->addAction([self::class, 'renderLatteUnknownMacro']);
|
||||
$blueScreen->addFileGenerator(function (string $file) {
|
||||
return substr($file, -6) === '.latte'
|
||||
? "{block content}\n\$END\$"
|
||||
: null;
|
||||
});
|
||||
Tracy\Debugger::addSourceMapper([self::class, 'mapLatteSourceCode']);
|
||||
}
|
||||
|
||||
$blueScreen->addAction([self::class, 'renderMemberAccessException']);
|
||||
$blueScreen->addPanel([self::class, 'renderNeonError']);
|
||||
}
|
||||
|
||||
|
||||
public static function renderLatteError(?\Throwable $e): ?array
|
||||
{
|
||||
if ($e instanceof Latte\CompileException && $e->sourceName) {
|
||||
return [
|
||||
'tab' => 'Template',
|
||||
'panel' => (preg_match('#\n|\?#', $e->sourceName)
|
||||
? ''
|
||||
: '<p>'
|
||||
. (@is_file($e->sourceName) // @ - may trigger error
|
||||
? '<b>File:</b> ' . Helpers::editorLink($e->sourceName, $e->sourceLine)
|
||||
: '<b>' . htmlspecialchars($e->sourceName . ($e->sourceLine ? ':' . $e->sourceLine : '')) . '</b>')
|
||||
. '</p>')
|
||||
. BlueScreen::highlightFile($e->sourceCode, $e->sourceLine, 15, false),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static function renderLatteUnknownMacro(?\Throwable $e): ?array
|
||||
{
|
||||
if (
|
||||
$e instanceof Latte\CompileException
|
||||
&& $e->sourceName
|
||||
&& @is_file($e->sourceName) // @ - may trigger error
|
||||
&& (preg_match('#Unknown macro (\{\w+)\}, did you mean (\{\w+)\}\?#A', $e->getMessage(), $m)
|
||||
|| preg_match('#Unknown attribute (n:\w+), did you mean (n:\w+)\?#A', $e->getMessage(), $m))
|
||||
) {
|
||||
return [
|
||||
'link' => Helpers::editorUri($e->sourceName, $e->sourceLine, 'fix', $m[1], $m[2]),
|
||||
'label' => 'fix it',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/** @return array{file: string, line: int, label: string, active: bool} */
|
||||
public static function mapLatteSourceCode(string $file, int $line): ?array
|
||||
{
|
||||
if (!strpos($file, '.latte--')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lines = file($file);
|
||||
if (
|
||||
!preg_match('#^/(?:\*\*|/) source: (\S+\.latte)#m', implode('', array_slice($lines, 0, 10)), $m)
|
||||
|| !@is_file($m[1]) // @ - may trigger error
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = $m[1];
|
||||
$line = $line && preg_match('#/\* line (\d+) \*/#', $lines[$line - 1], $m) ? (int) $m[1] : 0;
|
||||
return ['file' => $file, 'line' => $line, 'label' => 'Latte', 'active' => true];
|
||||
}
|
||||
|
||||
|
||||
public static function renderMemberAccessException(?\Throwable $e): ?array
|
||||
{
|
||||
if (!$e instanceof Nette\MemberAccessException && !$e instanceof \LogicException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$loc = $e->getTrace()[$e instanceof Nette\MemberAccessException ? 1 : 0];
|
||||
if (!isset($loc['file'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$loc = Tracy\Debugger::mapSource($loc['file'], $loc['line']) ?? $loc;
|
||||
if (preg_match('#Cannot (?:read|write to) an undeclared property .+::\$(\w+), did you mean \$(\w+)\?#A', $e->getMessage(), $m)) {
|
||||
return [
|
||||
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', '->' . $m[1], '->' . $m[2]),
|
||||
'label' => 'fix it',
|
||||
];
|
||||
} elseif (preg_match('#Call to undefined (static )?method .+::(\w+)\(\), did you mean (\w+)\(\)?#A', $e->getMessage(), $m)) {
|
||||
$operator = $m[1] ? '::' : '->';
|
||||
return [
|
||||
'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', $operator . $m[2] . '(', $operator . $m[3] . '('),
|
||||
'label' => 'fix it',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static function renderNeonError(?\Throwable $e): ?array
|
||||
{
|
||||
if (!$e instanceof Nette\Neon\Exception || !preg_match('#line (\d+)#', $e->getMessage(), $m)) {
|
||||
return null;
|
||||
|
||||
} elseif ($trace = Helpers::findTrace($e->getTrace(), [Nette\Neon\Decoder::class, 'decodeFile'])
|
||||
?? Helpers::findTrace($e->getTrace(), [Nette\DI\Config\Adapters\NeonAdapter::class, 'load'])
|
||||
) {
|
||||
$panel = '<p><b>File:</b> ' . Helpers::editorLink($trace['args'][0], (int) $m[1]) . '</p>'
|
||||
. self::highlightNeon(file_get_contents($trace['args'][0]), (int) $m[1]);
|
||||
|
||||
} elseif ($trace = Helpers::findTrace($e->getTrace(), [Nette\Neon\Decoder::class, 'decode'])) {
|
||||
$panel = self::highlightNeon($trace['args'][0], (int) $m[1]);
|
||||
}
|
||||
|
||||
return isset($panel) ? ['tab' => 'NEON', 'panel' => $panel] : null;
|
||||
}
|
||||
|
||||
|
||||
private static function highlightNeon(string $code, int $line): string
|
||||
{
|
||||
$code = htmlspecialchars($code, ENT_IGNORE, 'UTF-8');
|
||||
$code = str_replace(' ', "<span class='tracy-dump-whitespace'>·</span>", $code);
|
||||
$code = str_replace("\t", "<span class='tracy-dump-whitespace'>→ </span>", $code);
|
||||
return '<pre class=code><div>'
|
||||
. BlueScreen::highlightLine($code, $line)
|
||||
. '</div></pre>';
|
||||
}
|
||||
}
|
59
libs/Nette/Bridges/Nette/MailSender.php
Normal file
59
libs/Nette/Bridges/Nette/MailSender.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Bridges\Nette;
|
||||
|
||||
use Nette;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Tracy logger bridge for Nette Mail.
|
||||
*/
|
||||
class MailSender
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Nette\Mail\IMailer */
|
||||
private $mailer;
|
||||
|
||||
/** @var string|null sender of email notifications */
|
||||
private $fromEmail;
|
||||
|
||||
|
||||
public function __construct(Nette\Mail\IMailer $mailer, ?string $fromEmail = null)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->fromEmail = $fromEmail;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed $message
|
||||
*/
|
||||
public function send($message, string $email): void
|
||||
{
|
||||
$host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n'));
|
||||
|
||||
$mail = new Nette\Mail\Message;
|
||||
$mail->setHeader('X-Mailer', 'Tracy');
|
||||
if ($this->fromEmail || Nette\Utils\Validators::isEmail("noreply@$host")) {
|
||||
$mail->setFrom($this->fromEmail ?: "noreply@$host");
|
||||
}
|
||||
|
||||
foreach (explode(',', $email) as $item) {
|
||||
$mail->addTo(trim($item));
|
||||
}
|
||||
|
||||
$mail->setSubject('PHP: An error occurred on the server ' . $host);
|
||||
$mail->setBody(Tracy\Logger::formatMessage($message) . "\n\nsource: " . Tracy\Helpers::getSource());
|
||||
|
||||
$this->mailer->send($mail);
|
||||
}
|
||||
}
|
184
libs/Nette/Bridges/Nette/TracyExtension.php
Normal file
184
libs/Nette/Bridges/Nette/TracyExtension.php
Normal file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Bridges\Nette;
|
||||
|
||||
use Nette;
|
||||
use Nette\DI\Definitions\Statement;
|
||||
use Nette\Schema\Expect;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Tracy extension for Nette DI.
|
||||
*/
|
||||
class TracyExtension extends Nette\DI\CompilerExtension
|
||||
{
|
||||
private const ErrorSeverityPattern = 'E_(?:ALL|PARSE|STRICT|RECOVERABLE_ERROR|(?:CORE|COMPILE)_(?:ERROR|WARNING)|(?:USER_)?(?:ERROR|WARNING|NOTICE|DEPRECATED))';
|
||||
|
||||
/** @var bool */
|
||||
private $debugMode;
|
||||
|
||||
/** @var bool */
|
||||
private $cliMode;
|
||||
|
||||
|
||||
public function __construct(bool $debugMode = false, bool $cliMode = false)
|
||||
{
|
||||
$this->debugMode = $debugMode;
|
||||
$this->cliMode = $cliMode;
|
||||
}
|
||||
|
||||
|
||||
public function getConfigSchema(): Nette\Schema\Schema
|
||||
{
|
||||
$errorSeverity = Expect::string()->pattern(self::ErrorSeverityPattern);
|
||||
$errorSeverityExpr = Expect::string()->pattern('(' . self::ErrorSeverityPattern . '|[ &|~()])+');
|
||||
|
||||
return Expect::structure([
|
||||
'email' => Expect::anyOf(Expect::email(), Expect::listOf('email'))->dynamic(),
|
||||
'fromEmail' => Expect::email()->dynamic(),
|
||||
'emailSnooze' => Expect::string()->dynamic(),
|
||||
'logSeverity' => Expect::anyOf(Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
|
||||
'editor' => Expect::type('string|null')->dynamic(),
|
||||
'browser' => Expect::string()->dynamic(),
|
||||
'errorTemplate' => Expect::string()->dynamic(),
|
||||
'strictMode' => Expect::anyOf(Expect::bool(), Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
|
||||
'showBar' => Expect::bool()->dynamic(),
|
||||
'maxLength' => Expect::int()->dynamic(),
|
||||
'maxDepth' => Expect::int()->dynamic(),
|
||||
'maxItems' => Expect::int()->dynamic(),
|
||||
'keysToHide' => Expect::array(null)->dynamic(),
|
||||
'dumpTheme' => Expect::string()->dynamic(),
|
||||
'showLocation' => Expect::bool()->dynamic(),
|
||||
'scream' => Expect::anyOf(Expect::bool(), Expect::int(), $errorSeverityExpr, Expect::listOf($errorSeverity)),
|
||||
'bar' => Expect::listOf('string|Nette\DI\Definitions\Statement'),
|
||||
'blueScreen' => Expect::listOf('callable'),
|
||||
'editorMapping' => Expect::arrayOf('string')->dynamic()->default(null),
|
||||
'netteMailer' => Expect::bool(true),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function loadConfiguration()
|
||||
{
|
||||
$builder = $this->getContainerBuilder();
|
||||
|
||||
$builder->addDefinition($this->prefix('logger'))
|
||||
->setClass(Tracy\ILogger::class)
|
||||
->setFactory([Tracy\Debugger::class, 'getLogger']);
|
||||
|
||||
$builder->addDefinition($this->prefix('blueScreen'))
|
||||
->setFactory([Tracy\Debugger::class, 'getBlueScreen']);
|
||||
|
||||
$builder->addDefinition($this->prefix('bar'))
|
||||
->setFactory([Tracy\Debugger::class, 'getBar']);
|
||||
}
|
||||
|
||||
|
||||
public function afterCompile(Nette\PhpGenerator\ClassType $class)
|
||||
{
|
||||
$initialize = $this->initialization ?? new Nette\PhpGenerator\Closure;
|
||||
$initialize->addBody('if (!Tracy\Debugger::isEnabled()) { return; }');
|
||||
|
||||
$builder = $this->getContainerBuilder();
|
||||
|
||||
$logger = $builder->getDefinition($this->prefix('logger'));
|
||||
$initialize->addBody($builder->formatPhp('$logger = ?;', [$logger]));
|
||||
if (
|
||||
!$logger instanceof Nette\DI\Definitions\ServiceDefinition
|
||||
|| $logger->getFactory()->getEntity() !== [Tracy\Debugger::class, 'getLogger']
|
||||
) {
|
||||
$initialize->addBody('Tracy\Debugger::setLogger($logger);');
|
||||
}
|
||||
|
||||
$options = (array) $this->config;
|
||||
unset($options['bar'], $options['blueScreen'], $options['netteMailer']);
|
||||
|
||||
foreach (['logSeverity', 'strictMode', 'scream'] as $key) {
|
||||
if (is_string($options[$key]) || is_array($options[$key])) {
|
||||
$options[$key] = $this->parseErrorSeverity($options[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
if ($value !== null) {
|
||||
$tbl = [
|
||||
'keysToHide' => 'array_push(Tracy\Debugger::getBlueScreen()->keysToHide, ... ?)',
|
||||
'fromEmail' => 'if ($logger instanceof Tracy\Logger) $logger->fromEmail = ?',
|
||||
'emailSnooze' => 'if ($logger instanceof Tracy\Logger) $logger->emailSnooze = ?',
|
||||
];
|
||||
$initialize->addBody($builder->formatPhp(
|
||||
($tbl[$key] ?? 'Tracy\Debugger::$' . $key . ' = ?') . ';',
|
||||
Nette\DI\Helpers::filterArguments([$value])
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->config->netteMailer && $builder->getByType(Nette\Mail\IMailer::class)) {
|
||||
$initialize->addBody($builder->formatPhp('if ($logger instanceof Tracy\Logger) $logger->mailer = ?;', [
|
||||
[new Statement(Tracy\Bridges\Nette\MailSender::class, ['fromEmail' => $this->config->fromEmail]), 'send'],
|
||||
]));
|
||||
}
|
||||
|
||||
if ($this->debugMode) {
|
||||
foreach ($this->config->bar as $item) {
|
||||
if (is_string($item) && substr($item, 0, 1) === '@') {
|
||||
$item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]);
|
||||
} elseif (is_string($item)) {
|
||||
$item = new Statement($item);
|
||||
}
|
||||
|
||||
$initialize->addBody($builder->formatPhp(
|
||||
'$this->getService(?)->addPanel(?);',
|
||||
Nette\DI\Helpers::filterArguments([$this->prefix('bar'), $item])
|
||||
));
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->cliMode
|
||||
&& Tracy\Debugger::getSessionStorage() instanceof Tracy\NativeSession
|
||||
&& ($name = $builder->getByType(Nette\Http\Session::class))
|
||||
) {
|
||||
$initialize->addBody('$this->getService(?)->start();', [$name]);
|
||||
$initialize->addBody('Tracy\Debugger::dispatch();');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->config->blueScreen as $item) {
|
||||
$initialize->addBody($builder->formatPhp(
|
||||
'$this->getService(?)->addPanel(?);',
|
||||
Nette\DI\Helpers::filterArguments([$this->prefix('blueScreen'), $item])
|
||||
));
|
||||
}
|
||||
|
||||
if (empty($this->initialization)) {
|
||||
$class->getMethod('initialize')->addBody("($initialize)();");
|
||||
}
|
||||
|
||||
if (($dir = Tracy\Debugger::$logDirectory) && !is_writable($dir)) {
|
||||
throw new Nette\InvalidStateException("Make directory '$dir' writable.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
private function parseErrorSeverity($value): int
|
||||
{
|
||||
$value = implode('|', (array) $value);
|
||||
$res = (int) @parse_ini_string('e = ' . $value)['e']; // @ may fail
|
||||
if (!$res) {
|
||||
throw new Nette\InvalidStateException("Syntax error in expression '$value'");
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
62
libs/Nette/Bridges/Psr/PsrToTracyLoggerAdapter.php
Normal file
62
libs/Nette/Bridges/Psr/PsrToTracyLoggerAdapter.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Bridges\Psr;
|
||||
|
||||
use Psr;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Psr\Log\LoggerInterface to Tracy\ILogger adapter.
|
||||
*/
|
||||
class PsrToTracyLoggerAdapter implements Tracy\ILogger
|
||||
{
|
||||
/** Tracy logger level to PSR-3 log level mapping */
|
||||
private const LevelMap = [
|
||||
Tracy\ILogger::DEBUG => Psr\Log\LogLevel::DEBUG,
|
||||
Tracy\ILogger::INFO => Psr\Log\LogLevel::INFO,
|
||||
Tracy\ILogger::WARNING => Psr\Log\LogLevel::WARNING,
|
||||
Tracy\ILogger::ERROR => Psr\Log\LogLevel::ERROR,
|
||||
Tracy\ILogger::EXCEPTION => Psr\Log\LogLevel::ERROR,
|
||||
Tracy\ILogger::CRITICAL => Psr\Log\LogLevel::CRITICAL,
|
||||
];
|
||||
|
||||
/** @var Psr\Log\LoggerInterface */
|
||||
private $psrLogger;
|
||||
|
||||
|
||||
public function __construct(Psr\Log\LoggerInterface $psrLogger)
|
||||
{
|
||||
$this->psrLogger = $psrLogger;
|
||||
}
|
||||
|
||||
|
||||
public function log($value, $level = self::INFO)
|
||||
{
|
||||
if ($value instanceof \Throwable) {
|
||||
$message = Tracy\Helpers::getClass($value) . ': ' . $value->getMessage() . ($value->getCode() ? ' #' . $value->getCode() : '') . ' in ' . $value->getFile() . ':' . $value->getLine();
|
||||
$context = ['exception' => $value];
|
||||
|
||||
} elseif (!is_string($value)) {
|
||||
$message = trim(Tracy\Dumper::toText($value));
|
||||
$context = [];
|
||||
|
||||
} else {
|
||||
$message = $value;
|
||||
$context = [];
|
||||
}
|
||||
|
||||
$this->psrLogger->log(
|
||||
self::LevelMap[$level] ?? Psr\Log\LogLevel::ERROR,
|
||||
$message,
|
||||
$context
|
||||
);
|
||||
}
|
||||
}
|
61
libs/Nette/Bridges/Psr/TracyToPsrLoggerAdapter.php
Normal file
61
libs/Nette/Bridges/Psr/TracyToPsrLoggerAdapter.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Bridges\Psr;
|
||||
|
||||
use Psr;
|
||||
use Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Tracy\ILogger to Psr\Log\LoggerInterface adapter.
|
||||
*/
|
||||
class TracyToPsrLoggerAdapter extends Psr\Log\AbstractLogger
|
||||
{
|
||||
/** PSR-3 log level to Tracy logger level mapping */
|
||||
private const LevelMap = [
|
||||
Psr\Log\LogLevel::EMERGENCY => Tracy\ILogger::CRITICAL,
|
||||
Psr\Log\LogLevel::ALERT => Tracy\ILogger::CRITICAL,
|
||||
Psr\Log\LogLevel::CRITICAL => Tracy\ILogger::CRITICAL,
|
||||
Psr\Log\LogLevel::ERROR => Tracy\ILogger::ERROR,
|
||||
Psr\Log\LogLevel::WARNING => Tracy\ILogger::WARNING,
|
||||
Psr\Log\LogLevel::NOTICE => Tracy\ILogger::WARNING,
|
||||
Psr\Log\LogLevel::INFO => Tracy\ILogger::INFO,
|
||||
Psr\Log\LogLevel::DEBUG => Tracy\ILogger::DEBUG,
|
||||
];
|
||||
|
||||
/** @var Tracy\ILogger */
|
||||
private $tracyLogger;
|
||||
|
||||
|
||||
public function __construct(Tracy\ILogger $tracyLogger)
|
||||
{
|
||||
$this->tracyLogger = $tracyLogger;
|
||||
}
|
||||
|
||||
|
||||
public function log($level, $message, array $context = []): void
|
||||
{
|
||||
$level = self::LevelMap[$level] ?? Tracy\ILogger::ERROR;
|
||||
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$this->tracyLogger->log($context['exception'], $level);
|
||||
unset($context['exception']);
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
$message = [
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
];
|
||||
}
|
||||
|
||||
$this->tracyLogger->log($message, $level);
|
||||
}
|
||||
}
|
164
libs/Nette/Tracy/Bar/Bar.php
Normal file
164
libs/Nette/Tracy/Bar/Bar.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Debug Bar.
|
||||
*/
|
||||
class Bar
|
||||
{
|
||||
/** @var IBarPanel[] */
|
||||
private $panels = [];
|
||||
|
||||
/** @var bool */
|
||||
private $loaderRendered = false;
|
||||
|
||||
|
||||
/**
|
||||
* Add custom panel.
|
||||
* @return static
|
||||
*/
|
||||
public function addPanel(IBarPanel $panel, ?string $id = null): self
|
||||
{
|
||||
if ($id === null) {
|
||||
$c = 0;
|
||||
do {
|
||||
$id = get_class($panel) . ($c++ ? "-$c" : '');
|
||||
} while (isset($this->panels[$id]));
|
||||
}
|
||||
|
||||
$this->panels[$id] = $panel;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns panel with given id
|
||||
*/
|
||||
public function getPanel(string $id): ?IBarPanel
|
||||
{
|
||||
return $this->panels[$id] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders loading <script>
|
||||
* @internal
|
||||
*/
|
||||
public function renderLoader(DeferredContent $defer): void
|
||||
{
|
||||
if (!$defer->isAvailable()) {
|
||||
throw new \LogicException('Start session before Tracy is enabled.');
|
||||
}
|
||||
|
||||
$this->loaderRendered = true;
|
||||
$requestId = $defer->getRequestId();
|
||||
$nonce = Helpers::getNonce();
|
||||
$async = true;
|
||||
require __DIR__ . '/assets/loader.phtml';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders debug bar.
|
||||
*/
|
||||
public function render(DeferredContent $defer): void
|
||||
{
|
||||
$redirectQueue = &$defer->getItems('redirect');
|
||||
$requestId = $defer->getRequestId();
|
||||
|
||||
if (Helpers::isAjax()) {
|
||||
if ($defer->isAvailable()) {
|
||||
$defer->addSetup('Tracy.Debug.loadAjax', $this->renderPartial('ajax', '-ajax:' . $requestId));
|
||||
}
|
||||
} elseif (Helpers::isRedirect()) {
|
||||
if ($defer->isAvailable()) {
|
||||
$redirectQueue[] = ['content' => $this->renderPartial('redirect', '-r' . count($redirectQueue)), 'time' => time()];
|
||||
}
|
||||
} elseif (Helpers::isHtmlMode()) {
|
||||
if (preg_match('#^Content-Length:#im', implode("\n", headers_list()))) {
|
||||
Debugger::log(new \LogicException('Tracy cannot display the Bar because the Content-Length header is being sent'), Debugger::EXCEPTION);
|
||||
}
|
||||
|
||||
$content = $this->renderPartial('main');
|
||||
|
||||
foreach (array_reverse($redirectQueue) as $item) {
|
||||
$content['bar'] .= $item['content']['bar'];
|
||||
$content['panels'] .= $item['content']['panels'];
|
||||
}
|
||||
|
||||
$redirectQueue = null;
|
||||
|
||||
$content = '<div id=tracy-debug-bar>' . $content['bar'] . '</div>' . $content['panels'];
|
||||
|
||||
if ($this->loaderRendered) {
|
||||
$defer->addSetup('Tracy.Debug.init', $content);
|
||||
|
||||
} else {
|
||||
$nonce = Helpers::getNonce();
|
||||
$async = false;
|
||||
Debugger::removeOutputBuffers(false);
|
||||
require __DIR__ . '/assets/loader.phtml';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function renderPartial(string $type, string $suffix = ''): array
|
||||
{
|
||||
$panels = $this->renderPanels($suffix);
|
||||
|
||||
return [
|
||||
'bar' => Helpers::capture(function () use ($type, $panels) {
|
||||
require __DIR__ . '/assets/bar.phtml';
|
||||
}),
|
||||
'panels' => Helpers::capture(function () use ($type, $panels) {
|
||||
require __DIR__ . '/assets/panels.phtml';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function renderPanels(string $suffix = ''): array
|
||||
{
|
||||
set_error_handler(function (int $severity, string $message, string $file, int $line) {
|
||||
if (error_reporting() & $severity) {
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
}
|
||||
});
|
||||
|
||||
$obLevel = ob_get_level();
|
||||
$panels = [];
|
||||
|
||||
foreach ($this->panels as $id => $panel) {
|
||||
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
|
||||
try {
|
||||
$tab = (string) $panel->getTab();
|
||||
$panelHtml = $tab ? $panel->getPanel() : null;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
while (ob_get_level() > $obLevel) { // restore ob-level if broken
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$idHtml = "error-$idHtml";
|
||||
$tab = "Error in $id";
|
||||
$panelHtml = "<h1>Error: $id</h1><div class='tracy-inner'>" . nl2br(Helpers::escapeHtml($e)) . '</div>';
|
||||
unset($e);
|
||||
}
|
||||
|
||||
$panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml];
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
return $panels;
|
||||
}
|
||||
}
|
55
libs/Nette/Tracy/Bar/DefaultBarPanel.php
Normal file
55
libs/Nette/Tracy/Bar/DefaultBarPanel.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* IBarPanel implementation helper.
|
||||
* @internal
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
class DefaultBarPanel implements IBarPanel
|
||||
{
|
||||
public $data;
|
||||
|
||||
private $id;
|
||||
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders HTML code for custom tab.
|
||||
*/
|
||||
public function getTab(): string
|
||||
{
|
||||
return Helpers::capture(function () {
|
||||
$data = $this->data;
|
||||
require __DIR__ . "/panels/{$this->id}.tab.phtml";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders HTML code for custom panel.
|
||||
*/
|
||||
public function getPanel(): string
|
||||
{
|
||||
return Helpers::capture(function () {
|
||||
if (is_file(__DIR__ . "/panels/{$this->id}.panel.phtml")) {
|
||||
$data = $this->data;
|
||||
require __DIR__ . "/panels/{$this->id}.panel.phtml";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
29
libs/Nette/Tracy/Bar/IBarPanel.php
Normal file
29
libs/Nette/Tracy/Bar/IBarPanel.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Custom output for Debugger.
|
||||
*/
|
||||
interface IBarPanel
|
||||
{
|
||||
/**
|
||||
* Renders HTML code for custom tab.
|
||||
* @return string
|
||||
*/
|
||||
function getTab();
|
||||
|
||||
/**
|
||||
* Renders HTML code for custom panel.
|
||||
* @return string
|
||||
*/
|
||||
function getPanel();
|
||||
}
|
291
libs/Nette/Tracy/Bar/assets/bar.css
Normal file
291
libs/Nette/Tracy/Bar/assets/bar.css
Normal file
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
/* common styles */
|
||||
#tracy-debug {
|
||||
display: none;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
body#tracy-debug { /* in popup window */
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tracy-debug:not(body) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#tracy-debug a {
|
||||
color: #125EAE;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#tracy-debug a:hover,
|
||||
#tracy-debug a:focus {
|
||||
background-color: #125EAE;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tracy-debug h2,
|
||||
#tracy-debug h3,
|
||||
#tracy-debug p {
|
||||
margin: .4em 0;
|
||||
}
|
||||
|
||||
#tracy-debug table {
|
||||
background: #FDF5CE;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tracy-debug tr:nth-child(2n) td {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
#tracy-debug td,
|
||||
#tracy-debug th {
|
||||
border: 1px solid #E6DFBF;
|
||||
padding: 2px 5px;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#tracy-debug th {
|
||||
background: #F4F3F1;
|
||||
color: #655E5E;
|
||||
font-size: 90%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tracy-debug pre,
|
||||
#tracy-debug code {
|
||||
font: 9pt/1.5 Consolas, monospace;
|
||||
}
|
||||
|
||||
#tracy-debug table .tracy-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#tracy-debug svg {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-dump {
|
||||
margin: 0;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
|
||||
/* bar */
|
||||
#tracy-debug-bar {
|
||||
font: normal normal 13px/1.55 Tahoma, sans-serif;
|
||||
color: #333;
|
||||
border: 1px solid #c9c9c9;
|
||||
background: #EDEAE0 url('data:image/png;base64,R0lGODlhAQAUALMAAOzq4e/t5e7s4/Dt5vDu5e3r4vDu5uvp4O/t5AAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAABABQAAAQM0EgySEAYi1LA+UcEADs=') top;
|
||||
background-size: 1em;
|
||||
position: fixed;
|
||||
|
||||
min-width: 50px;
|
||||
white-space: nowrap;
|
||||
|
||||
z-index: 30000;
|
||||
opacity: .9;
|
||||
transition: opacity 0.2s;
|
||||
will-change: opacity, top, left;
|
||||
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 10px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
#tracy-debug-bar:hover {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
#tracy-debug-bar .tracy-row {
|
||||
list-style: none none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#tracy-debug-bar .tracy-row:not(:first-child) {
|
||||
background: #d5d2c6;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
#tracy-debug-bar .tracy-row[data-tracy-group="ajax"] {
|
||||
animation: tracy-row-flash .2s ease;
|
||||
}
|
||||
|
||||
@keyframes tracy-row-flash {
|
||||
0% {
|
||||
background: #c9c0a0;
|
||||
}
|
||||
}
|
||||
|
||||
#tracy-debug-bar .tracy-row:not(:first-child) li:first-child {
|
||||
width: 4.1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#tracy-debug-bar img {
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
#tracy-debug-bar svg {
|
||||
vertical-align: bottom;
|
||||
width: 1.23em;
|
||||
height: 1.55em;
|
||||
}
|
||||
|
||||
#tracy-debug-bar .tracy-label {
|
||||
margin-left: .2em;
|
||||
}
|
||||
|
||||
#tracy-debug-bar li > a,
|
||||
#tracy-debug-bar li > span {
|
||||
color: #000;
|
||||
display: block;
|
||||
padding: 0 .4em;
|
||||
}
|
||||
|
||||
#tracy-debug-bar li > a:hover {
|
||||
color: black;
|
||||
background: #c3c1b8;
|
||||
}
|
||||
|
||||
#tracy-debug-bar li:first-child {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#tracy-debug-logo svg {
|
||||
width: 3.4em;
|
||||
margin: 0 .2em 0 .5em;
|
||||
}
|
||||
|
||||
|
||||
/* panels */
|
||||
#tracy-debug .tracy-panel {
|
||||
display: none;
|
||||
font: normal normal 12px/1.5 sans-serif;
|
||||
background: white;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body#tracy-debug .tracy-panel { /* in popup window */
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tracy-debug h1 {
|
||||
font: normal normal 23px/1.4 Tahoma, sans-serif;
|
||||
color: #575753;
|
||||
margin: -5px -5px 5px;
|
||||
padding: 0 5px 0 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel .tracy-icons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel-ajax h1::after,
|
||||
#tracy-debug .tracy-panel-redirect h1::after {
|
||||
content: 'ajax';
|
||||
float: right;
|
||||
font-size: 65%;
|
||||
margin: 0 .3em;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-panel-redirect h1::after {
|
||||
content: 'redirect';
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float {
|
||||
position: fixed;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 80px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 1px 1px 20px rgba(102, 102, 102, 0.36);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
|
||||
max-width: 700px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-height: 555px) {
|
||||
#tracy-debug .tracy-mode-peek,
|
||||
#tracy-debug .tracy-mode-float:not(.tracy-panel-resized) {
|
||||
max-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-peek h1 {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float {
|
||||
display: flex;
|
||||
opacity: .95;
|
||||
transition: opacity 0.2s;
|
||||
will-change: opacity, top, left;
|
||||
overflow: auto;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-focused {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float h1 {
|
||||
cursor: move;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-float .tracy-icons {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-mode-window {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-icons a {
|
||||
color: #575753;
|
||||
}
|
||||
|
||||
#tracy-debug .tracy-icons a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
#tracy-debug .tracy-inner-container {
|
||||
min-width: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#tracy-debug * {
|
||||
display: none;
|
||||
}
|
||||
}
|
685
libs/Nette/Tracy/Bar/assets/bar.js
Normal file
685
libs/Nette/Tracy/Bar/assets/bar.js
Normal file
@ -0,0 +1,685 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
let nonce = document.currentScript.getAttribute('nonce') || document.currentScript.nonce,
|
||||
requestId = document.currentScript.dataset.id,
|
||||
ajaxCounter = 1,
|
||||
baseUrl = location.href.split('#')[0];
|
||||
|
||||
baseUrl += (baseUrl.indexOf('?') < 0 ? '?' : '&');
|
||||
|
||||
let defaults = {
|
||||
PanelZIndex: 20000,
|
||||
MaxAjaxRows: 3,
|
||||
AutoRefresh: true,
|
||||
};
|
||||
|
||||
function getOption(key)
|
||||
{
|
||||
let global = window['Tracy' + key];
|
||||
return global === undefined ? defaults[key] : global;
|
||||
}
|
||||
|
||||
class Panel
|
||||
{
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.elem = document.getElementById(this.id);
|
||||
this.elem.Tracy = this.elem.Tracy || {};
|
||||
}
|
||||
|
||||
|
||||
init() {
|
||||
let elem = this.elem;
|
||||
|
||||
this.init = function() {};
|
||||
elem.innerHTML = elem.dataset.tracyContent;
|
||||
Tracy.Dumper.init(Debug.layer);
|
||||
evalScripts(elem);
|
||||
|
||||
draggable(elem, {
|
||||
handles: elem.querySelectorAll('h1'),
|
||||
start: () => {
|
||||
if (!this.is(Panel.FLOAT)) {
|
||||
this.toFloat();
|
||||
}
|
||||
this.focus();
|
||||
this.peekPosition = false;
|
||||
}
|
||||
});
|
||||
|
||||
elem.addEventListener('mousedown', () => {
|
||||
this.focus();
|
||||
});
|
||||
|
||||
elem.addEventListener('mouseenter', () => {
|
||||
clearTimeout(elem.Tracy.displayTimeout);
|
||||
});
|
||||
|
||||
elem.addEventListener('mouseleave', () => {
|
||||
this.blur();
|
||||
});
|
||||
|
||||
elem.addEventListener('mousemove', (e) => {
|
||||
if (e.buttons && !this.is(Panel.RESIZED) && (elem.style.width || elem.style.height)) {
|
||||
elem.classList.add(Panel.RESIZED);
|
||||
}
|
||||
});
|
||||
|
||||
elem.addEventListener('tracy-toggle', () => {
|
||||
this.reposition();
|
||||
});
|
||||
|
||||
elem.querySelectorAll('.tracy-icons a').forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
if (link.dataset.tracyAction === 'close') {
|
||||
this.toPeek();
|
||||
} else if (link.dataset.tracyAction === 'window') {
|
||||
this.toWindow();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
});
|
||||
|
||||
if (this.is('tracy-panel-persist')) {
|
||||
Tracy.Toggle.persist(elem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
is(mode) {
|
||||
return this.elem.classList.contains(mode);
|
||||
}
|
||||
|
||||
|
||||
focus() {
|
||||
let elem = this.elem;
|
||||
if (this.is(Panel.WINDOW)) {
|
||||
elem.Tracy.window.focus();
|
||||
|
||||
} else if (!this.is(Panel.FOCUSED)) {
|
||||
for (let id in Debug.panels) {
|
||||
Debug.panels[id].elem.classList.remove(Panel.FOCUSED);
|
||||
}
|
||||
elem.classList.add(Panel.FOCUSED);
|
||||
elem.style.zIndex = getOption('PanelZIndex') + Panel.zIndexCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
blur() {
|
||||
let elem = this.elem;
|
||||
if (this.is(Panel.PEEK)) {
|
||||
clearTimeout(elem.Tracy.displayTimeout);
|
||||
elem.Tracy.displayTimeout = setTimeout(() => {
|
||||
elem.classList.remove(Panel.FOCUSED);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
toFloat() {
|
||||
this.elem.classList.remove(Panel.WINDOW);
|
||||
this.elem.classList.remove(Panel.PEEK);
|
||||
this.elem.classList.add(Panel.FLOAT);
|
||||
this.elem.classList.remove(Panel.RESIZED);
|
||||
this.reposition();
|
||||
}
|
||||
|
||||
|
||||
toPeek() {
|
||||
this.elem.classList.remove(Panel.WINDOW);
|
||||
this.elem.classList.remove(Panel.FLOAT);
|
||||
this.elem.classList.remove(Panel.FOCUSED);
|
||||
this.elem.classList.add(Panel.PEEK);
|
||||
this.elem.style.width = '';
|
||||
this.elem.style.height = '';
|
||||
this.elem.classList.remove(Panel.RESIZED);
|
||||
}
|
||||
|
||||
|
||||
toWindow() {
|
||||
let offset = getOffset(this.elem);
|
||||
offset.left += typeof window.screenLeft === 'number' ? window.screenLeft : (window.screenX + 10);
|
||||
offset.top += typeof window.screenTop === 'number' ? window.screenTop : (window.screenY + 50);
|
||||
|
||||
let win = window.open('', this.id.replace(/-/g, '_'), 'left=' + offset.left + ',top=' + offset.top
|
||||
+ ',width=' + this.elem.offsetWidth + ',height=' + this.elem.offsetHeight + ',resizable=yes,scrollbars=yes');
|
||||
if (!win) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let doc = win.document;
|
||||
doc.write('<!DOCTYPE html><meta charset="utf-8">'
|
||||
+ '<script src="' + (baseUrl.replace(/&/g, '&').replace(/"/g, '"')) + '_tracy_bar=js&XDEBUG_SESSION_STOP=1" onload="Tracy.Dumper.init()" async></script>'
|
||||
+ '<body id="tracy-debug">'
|
||||
);
|
||||
|
||||
let meta = this.elem.parentElement.lastElementChild;
|
||||
doc.body.innerHTML = '<tracy-div itemscope>'
|
||||
+ '<div class="tracy-panel tracy-mode-window" id="' + this.elem.id + '">' + this.elem.dataset.tracyContent + '</div>'
|
||||
+ meta.outerHTML
|
||||
+ '</tracy-div>';
|
||||
evalScripts(doc.body);
|
||||
if (this.elem.querySelector('h1')) {
|
||||
doc.title = this.elem.querySelector('h1').textContent;
|
||||
}
|
||||
|
||||
win.addEventListener('beforeunload', () => {
|
||||
this.toPeek();
|
||||
win.close(); // forces closing, can be invoked by F5
|
||||
});
|
||||
|
||||
doc.addEventListener('keyup', (e) => {
|
||||
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
||||
win.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.elem.classList.remove(Panel.FLOAT);
|
||||
this.elem.classList.remove(Panel.PEEK);
|
||||
this.elem.classList.remove(Panel.FOCUSED);
|
||||
this.elem.classList.remove(Panel.RESIZED);
|
||||
this.elem.classList.add(Panel.WINDOW);
|
||||
this.elem.Tracy.window = win;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
reposition(deltaX, deltaY) {
|
||||
let pos = getPosition(this.elem);
|
||||
if (pos.width) { // is visible?
|
||||
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
|
||||
if (this.is(Panel.RESIZED)) {
|
||||
let size = getWindowSize();
|
||||
this.elem.style.width = Math.min(size.width, pos.width) + 'px';
|
||||
this.elem.style.height = Math.min(size.height, pos.height) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
savePosition() {
|
||||
let key = this.id.split(':')[0]; // remove :requestId part
|
||||
let pos = getPosition(this.elem);
|
||||
if (this.is(Panel.WINDOW)) {
|
||||
localStorage.setItem(key, JSON.stringify({window: true}));
|
||||
} else if (pos.width) { // is visible?
|
||||
localStorage.setItem(key, JSON.stringify({right: pos.right, bottom: pos.bottom, width: pos.width, height: pos.height, zIndex: this.elem.style.zIndex - getOption('PanelZIndex'), resized: this.is(Panel.RESIZED)}));
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
restorePosition() {
|
||||
let key = this.id.split(':')[0];
|
||||
let pos = JSON.parse(localStorage.getItem(key));
|
||||
if (!pos) {
|
||||
this.elem.classList.add(Panel.PEEK);
|
||||
} else if (pos.window) {
|
||||
this.init();
|
||||
this.toWindow() || this.toFloat();
|
||||
} else if (this.elem.dataset.tracyContent) {
|
||||
this.init();
|
||||
this.toFloat();
|
||||
if (pos.resized) {
|
||||
this.elem.classList.add(Panel.RESIZED);
|
||||
this.elem.style.width = pos.width + 'px';
|
||||
this.elem.style.height = pos.height + 'px';
|
||||
}
|
||||
setPosition(this.elem, pos);
|
||||
this.elem.style.zIndex = getOption('PanelZIndex') + (pos.zIndex || 1);
|
||||
Panel.zIndexCounter = Math.max(Panel.zIndexCounter, (pos.zIndex || 1)) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Panel.PEEK = 'tracy-mode-peek';
|
||||
Panel.FLOAT = 'tracy-mode-float';
|
||||
Panel.WINDOW = 'tracy-mode-window';
|
||||
Panel.FOCUSED = 'tracy-focused';
|
||||
Panel.RESIZED = 'tracy-panel-resized';
|
||||
Panel.zIndexCounter = 1;
|
||||
|
||||
|
||||
class Bar
|
||||
{
|
||||
init() {
|
||||
this.id = 'tracy-debug-bar';
|
||||
this.elem = document.getElementById(this.id);
|
||||
|
||||
draggable(this.elem, {
|
||||
handles: this.elem.querySelectorAll('li:first-child'),
|
||||
draggedClass: 'tracy-dragged',
|
||||
stop: () => {
|
||||
this.savePosition();
|
||||
}
|
||||
});
|
||||
|
||||
this.elem.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.initTabs(this.elem);
|
||||
this.restorePosition();
|
||||
|
||||
(new MutationObserver(() => {
|
||||
this.restorePosition();
|
||||
})).observe(this.elem, {childList: true, characterData: true, subtree: true});
|
||||
}
|
||||
|
||||
|
||||
initTabs(elem) {
|
||||
elem.querySelectorAll('a').forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
if (link.dataset.tracyAction === 'close') {
|
||||
this.close();
|
||||
|
||||
} else if (link.rel) {
|
||||
let panel = Debug.panels[link.rel];
|
||||
panel.init();
|
||||
|
||||
if (e.shiftKey) {
|
||||
panel.toFloat();
|
||||
panel.toWindow();
|
||||
|
||||
} else if (panel.is(Panel.FLOAT)) {
|
||||
panel.toPeek();
|
||||
|
||||
} else {
|
||||
panel.toFloat();
|
||||
if (panel.peekPosition) {
|
||||
panel.reposition(-Math.round(Math.random() * 100) - 20, (Math.round(Math.random() * 100) + 20) * (this.isAtTop() ? 1 : -1));
|
||||
panel.peekPosition = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
|
||||
link.addEventListener('mouseenter', (e) => {
|
||||
if (e.buttons || !link.rel || elem.classList.contains('tracy-dragged')) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.displayTimeout);
|
||||
this.displayTimeout = setTimeout(() => {
|
||||
let panel = Debug.panels[link.rel];
|
||||
panel.focus();
|
||||
|
||||
if (panel.is(Panel.PEEK)) {
|
||||
panel.init();
|
||||
|
||||
let pos = getPosition(panel.elem);
|
||||
setPosition(panel.elem, {
|
||||
left: getOffset(link).left + getPosition(link).width + 4 - pos.width,
|
||||
top: this.isAtTop()
|
||||
? getOffset(this.elem).top + getPosition(this.elem).height + 4
|
||||
: getOffset(this.elem).top - pos.height - 4
|
||||
});
|
||||
panel.peekPosition = true;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', () => {
|
||||
clearTimeout(this.displayTimeout);
|
||||
|
||||
if (link.rel && !elem.classList.contains('tracy-dragged')) {
|
||||
Debug.panels[link.rel].blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.autoHideLabels();
|
||||
}
|
||||
|
||||
|
||||
autoHideLabels() {
|
||||
let width = getWindowSize().width;
|
||||
this.elem.querySelectorAll('.tracy-row').forEach((row) => {
|
||||
let i, labels = row.querySelectorAll('.tracy-label');
|
||||
for (i = 0; i < labels.length && row.clientWidth < width; i++) {
|
||||
labels.item(i).hidden = false;
|
||||
}
|
||||
for (i = labels.length - 1; i >= 0 && row.clientWidth >= width; i--) {
|
||||
labels.item(i).hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
document.getElementById('tracy-debug').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
reposition(deltaX, deltaY) {
|
||||
let pos = getPosition(this.elem);
|
||||
if (pos.width) { // is visible?
|
||||
setPosition(this.elem, {left: pos.left + (deltaX || 0), top: pos.top + (deltaY || 0)});
|
||||
this.savePosition();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
savePosition() {
|
||||
let pos = getPosition(this.elem);
|
||||
if (pos.width) { // is visible?
|
||||
localStorage.setItem(this.id, JSON.stringify(this.isAtTop() ? {right: pos.right, top: pos.top} : {right: pos.right, bottom: pos.bottom}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
restorePosition() {
|
||||
let pos = JSON.parse(localStorage.getItem(this.id));
|
||||
setPosition(this.elem, pos || {right: 0, bottom: 0});
|
||||
this.savePosition();
|
||||
}
|
||||
|
||||
|
||||
isAtTop() {
|
||||
let pos = getPosition(this.elem);
|
||||
return pos.top < 100 && pos.bottom > pos.top;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Debug
|
||||
{
|
||||
static init(content) {
|
||||
Debug.bar = new Bar;
|
||||
Debug.panels = {};
|
||||
Debug.layer = document.createElement('tracy-div');
|
||||
Debug.layer.setAttribute('id', 'tracy-debug');
|
||||
Debug.layer.innerHTML = content;
|
||||
(document.body || document.documentElement).appendChild(Debug.layer);
|
||||
evalScripts(Debug.layer);
|
||||
Debug.layer.style.display = 'block';
|
||||
Debug.bar.init();
|
||||
|
||||
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
|
||||
Debug.panels[panel.id] = new Panel(panel.id);
|
||||
Debug.panels[panel.id].restorePosition();
|
||||
});
|
||||
|
||||
Debug.captureWindow();
|
||||
Debug.captureAjax();
|
||||
|
||||
Tracy.TableSort.init();
|
||||
}
|
||||
|
||||
|
||||
static loadAjax(content) {
|
||||
let rows = Debug.bar.elem.querySelectorAll('.tracy-row[data-tracy-group=ajax]');
|
||||
rows = Array.from(rows).reverse();
|
||||
let max = getOption('MaxAjaxRows');
|
||||
rows.forEach((row) => {
|
||||
if (--max > 0) {
|
||||
return;
|
||||
}
|
||||
row.querySelectorAll('a[rel]').forEach((tab) => {
|
||||
let panel = Debug.panels[tab.rel];
|
||||
if (panel.is(Panel.PEEK)) {
|
||||
delete Debug.panels[tab.rel];
|
||||
panel.elem.remove();
|
||||
}
|
||||
});
|
||||
row.remove();
|
||||
});
|
||||
|
||||
if (rows[0]) { // update content in first-row panels
|
||||
rows[0].querySelectorAll('a[rel]').forEach((tab) => {
|
||||
Debug.panels[tab.rel].savePosition();
|
||||
Debug.panels[tab.rel].toPeek();
|
||||
});
|
||||
}
|
||||
|
||||
Debug.layer.insertAdjacentHTML('beforeend', content.panels);
|
||||
evalScripts(Debug.layer);
|
||||
Debug.bar.elem.insertAdjacentHTML('beforeend', content.bar);
|
||||
let ajaxBar = Debug.bar.elem.querySelector('.tracy-row:last-child');
|
||||
|
||||
Debug.layer.querySelectorAll('.tracy-panel').forEach((panel) => {
|
||||
if (!Debug.panels[panel.id]) {
|
||||
Debug.panels[panel.id] = new Panel(panel.id);
|
||||
Debug.panels[panel.id].restorePosition();
|
||||
}
|
||||
});
|
||||
|
||||
Debug.bar.initTabs(ajaxBar);
|
||||
}
|
||||
|
||||
|
||||
static captureWindow() {
|
||||
let size = getWindowSize();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
let newSize = getWindowSize();
|
||||
|
||||
Debug.bar.reposition(newSize.width - size.width, newSize.height - size.height);
|
||||
Debug.bar.autoHideLabels();
|
||||
|
||||
for (let id in Debug.panels) {
|
||||
Debug.panels[id].reposition(newSize.width - size.width, newSize.height - size.height);
|
||||
}
|
||||
|
||||
size = newSize;
|
||||
});
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
for (let id in Debug.panels) {
|
||||
Debug.panels[id].savePosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static captureAjax() {
|
||||
if (!requestId) {
|
||||
return;
|
||||
}
|
||||
let oldOpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
XMLHttpRequest.prototype.open = function() {
|
||||
oldOpen.apply(this, arguments);
|
||||
|
||||
if (getOption('AutoRefresh') && new URL(arguments[1], location.origin).host === location.host) {
|
||||
let reqId = Tracy.getAjaxHeader();
|
||||
this.setRequestHeader('X-Tracy-Ajax', reqId);
|
||||
this.addEventListener('load', function() {
|
||||
if (this.getAllResponseHeaders().match(/^X-Tracy-Ajax: 1/mi)) {
|
||||
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let oldFetch = window.fetch;
|
||||
window.fetch = function(request, options) {
|
||||
request = request instanceof Request ? request : new Request(request, options || {});
|
||||
let reqId = request.headers.get('X-Tracy-Ajax');
|
||||
|
||||
if (getOption('AutoRefresh') && !reqId && new URL(request.url, location.origin).host === location.host) {
|
||||
reqId = Tracy.getAjaxHeader();
|
||||
request.headers.set('X-Tracy-Ajax', reqId);
|
||||
}
|
||||
|
||||
return oldFetch(request).then((response) => {
|
||||
if (response instanceof Response && response.headers.has('X-Tracy-Ajax') && response.headers.get('X-Tracy-Ajax')[0] === '1') {
|
||||
Debug.loadScript(baseUrl + '_tracy_bar=content-ajax.' + reqId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random());
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static loadScript(url) {
|
||||
if (Debug.scriptElem) {
|
||||
Debug.scriptElem.remove();
|
||||
}
|
||||
Debug.scriptElem = document.createElement('script');
|
||||
Debug.scriptElem.src = url;
|
||||
Debug.scriptElem.setAttribute('nonce', nonce);
|
||||
(document.body || document.documentElement).appendChild(Debug.scriptElem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function evalScripts(elem) {
|
||||
elem.querySelectorAll('script').forEach((script) => {
|
||||
if ((!script.hasAttribute('type') || script.type === 'text/javascript' || script.type === 'application/javascript') && !script.tracyEvaluated) {
|
||||
let document = script.ownerDocument;
|
||||
let dolly = document.createElement('script');
|
||||
dolly.textContent = script.textContent;
|
||||
dolly.setAttribute('nonce', nonce);
|
||||
(document.body || document.documentElement).appendChild(dolly);
|
||||
script.tracyEvaluated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let dragging;
|
||||
|
||||
function draggable(elem, options) {
|
||||
let dE = document.documentElement, started, deltaX, deltaY, clientX, clientY;
|
||||
options = options || {};
|
||||
|
||||
let redraw = function () {
|
||||
if (dragging) {
|
||||
setPosition(elem, {left: clientX + deltaX, top: clientY + deltaY});
|
||||
requestAnimationFrame(redraw);
|
||||
}
|
||||
};
|
||||
|
||||
let onMove = function(e) {
|
||||
if (e.buttons === 0) {
|
||||
return onEnd(e);
|
||||
}
|
||||
if (!started) {
|
||||
if (options.draggedClass) {
|
||||
elem.classList.add(options.draggedClass);
|
||||
}
|
||||
if (options.start) {
|
||||
options.start(e, elem);
|
||||
}
|
||||
started = true;
|
||||
}
|
||||
|
||||
clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
return false;
|
||||
};
|
||||
|
||||
let onEnd = function(e) {
|
||||
if (started) {
|
||||
if (options.draggedClass) {
|
||||
elem.classList.remove(options.draggedClass);
|
||||
}
|
||||
if (options.stop) {
|
||||
options.stop(e, elem);
|
||||
}
|
||||
}
|
||||
dragging = null;
|
||||
dE.removeEventListener('mousemove', onMove);
|
||||
dE.removeEventListener('mouseup', onEnd);
|
||||
dE.removeEventListener('touchmove', onMove);
|
||||
dE.removeEventListener('touchend', onEnd);
|
||||
return false;
|
||||
};
|
||||
|
||||
let onStart = function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (dragging) { // missed mouseup out of window?
|
||||
return onEnd(e);
|
||||
}
|
||||
|
||||
let pos = getPosition(elem);
|
||||
clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
deltaX = pos.left - clientX;
|
||||
deltaY = pos.top - clientY;
|
||||
dragging = true;
|
||||
started = false;
|
||||
dE.addEventListener('mousemove', onMove);
|
||||
dE.addEventListener('mouseup', onEnd);
|
||||
dE.addEventListener('touchmove', onMove);
|
||||
dE.addEventListener('touchend', onEnd);
|
||||
requestAnimationFrame(redraw);
|
||||
if (options.start) {
|
||||
options.start(e, elem);
|
||||
}
|
||||
};
|
||||
|
||||
options.handles.forEach((handle) => {
|
||||
handle.addEventListener('mousedown', onStart);
|
||||
handle.addEventListener('touchstart', onStart);
|
||||
|
||||
handle.addEventListener('click', (e) => {
|
||||
if (started) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// returns total offset for element
|
||||
function getOffset(elem) {
|
||||
let res = {left: elem.offsetLeft, top: elem.offsetTop};
|
||||
while (elem = elem.offsetParent) { // eslint-disable-line no-cond-assign
|
||||
res.left += elem.offsetLeft; res.top += elem.offsetTop;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
function getWindowSize() {
|
||||
return {
|
||||
width: document.documentElement.clientWidth,
|
||||
height: document.compatMode === 'BackCompat' ? window.innerHeight : document.documentElement.clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// move to new position
|
||||
function setPosition(elem, coords) {
|
||||
let win = getWindowSize();
|
||||
if (typeof coords.right !== 'undefined') {
|
||||
coords.left = win.width - elem.offsetWidth - coords.right;
|
||||
}
|
||||
if (typeof coords.bottom !== 'undefined') {
|
||||
coords.top = win.height - elem.offsetHeight - coords.bottom;
|
||||
}
|
||||
elem.style.left = Math.max(0, Math.min(coords.left, win.width - elem.offsetWidth)) + 'px';
|
||||
elem.style.top = Math.max(0, Math.min(coords.top, win.height - elem.offsetHeight)) + 'px';
|
||||
}
|
||||
|
||||
|
||||
// returns current position
|
||||
function getPosition(elem) {
|
||||
let win = getWindowSize();
|
||||
return {
|
||||
left: elem.offsetLeft,
|
||||
top: elem.offsetTop,
|
||||
right: win.width - elem.offsetWidth - elem.offsetLeft,
|
||||
bottom: win.height - elem.offsetHeight - elem.offsetTop,
|
||||
width: elem.offsetWidth,
|
||||
height: elem.offsetHeight
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
let Tracy = window.Tracy = window.Tracy || {};
|
||||
Tracy.DebugPanel = Panel;
|
||||
Tracy.DebugBar = Bar;
|
||||
Tracy.Debug = Debug;
|
||||
Tracy.getAjaxHeader = () => requestId + '_' + ajaxCounter++;
|
31
libs/Nette/Tracy/Bar/assets/bar.phtml
Normal file
31
libs/Nette/Tracy/Bar/assets/bar.phtml
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var string $type
|
||||
* @var \stdClass[] $panels
|
||||
*/
|
||||
?>
|
||||
|
||||
<ul class="tracy-row" data-tracy-group="<?= Helpers::escapeHtml($type) ?>">
|
||||
<?php if ($type === 'main'): ?>
|
||||
<li id="tracy-debug-logo" title="Tracy Debugger <?= Debugger::VERSION, " \nhttps://tracy.nette.org" ?>">
|
||||
<svg viewBox="0 -10 1561 333"><path fill="#585755" d="m176 327h-57v-269h-119v-57h291v57h-115v269zm208-191h114c50 0 47-78 0-78h-114v78zm106-135c17 0 33 2 46 7 75 30 75 144 1 175-13 6-29 8-47 8h-27l132 74v68l-211-128v122h-57v-326h163zm300 57c-5 0-9 3-11 9l-56 156h135l-55-155c-2-7-6-10-13-10zm-86 222l-17 47h-61l102-285c20-56 107-56 126 0l102 285h-61l-17-47h-174zm410 47c-98 0-148-55-148-163v-2c0-107 50-161 149-161h118v57h-133c-26 0-45 8-58 25-12 17-19 44-19 81 0 71 26 106 77 106h133v57h-119zm270-145l-121-181h68l81 130 81-130h68l-121 178v148h-56v-145z"/></svg>
|
||||
</li>
|
||||
<?php endif; if ($type === 'redirect'): ?>
|
||||
<li><span title="Previous request before redirect">redirect</span></li>
|
||||
<?php endif; if ($type === 'ajax'): ?>
|
||||
<li>AJAX</li>
|
||||
<?php endif ?>
|
||||
|
||||
<?php foreach ($panels as $panel): if ($panel->tab) { ?>
|
||||
<li><?php if ($panel->panel): ?><a href="#" rel="tracy-debug-panel-<?= $panel->id ?>"><?= trim($panel->tab) ?></a><?php else: echo '<span>', trim($panel->tab), '</span>'; endif ?></li>
|
||||
<?php } endforeach ?>
|
||||
|
||||
<?php if ($type === 'main'): ?>
|
||||
<li><a href="#" data-tracy-action="close" title="close debug bar">×</a></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
30
libs/Nette/Tracy/Bar/assets/loader.phtml
Normal file
30
libs/Nette/Tracy/Bar/assets/loader.phtml
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var ?string $nonce
|
||||
* @var bool $async
|
||||
* @var string $requestId
|
||||
*/
|
||||
|
||||
$baseUrl = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$baseUrl .= strpos($baseUrl, '?') === false ? '?' : '&';
|
||||
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
|
||||
$asyncAttr = $async ? ' async' : '';
|
||||
?>
|
||||
<?php if (empty($content)): ?>
|
||||
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=<?= urlencode("content.$requestId") ?>&XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $asyncAttr, $nonceAttr ?>></script>
|
||||
<?php else: ?>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Tracy Debug Bar -->
|
||||
<script src="<?= Helpers::escapeHtml($baseUrl) ?>_tracy_bar=js&v=<?= urlencode(Debugger::VERSION) ?>&XDEBUG_SESSION_STOP=1" data-id="<?= Helpers::escapeHtml($requestId) ?>"<?= $nonceAttr ?>></script>
|
||||
<script<?= $nonceAttr ?>>
|
||||
Tracy.Debug.init(<?= str_replace(['<!--', '</s'], ['<\!--', '<\/s'], json_encode($content, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE)) ?>);
|
||||
</script>
|
||||
<?php endif ?>
|
30
libs/Nette/Tracy/Bar/assets/panels.phtml
Normal file
30
libs/Nette/Tracy/Bar/assets/panels.phtml
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use Tracy\Helpers;
|
||||
|
||||
/**
|
||||
* @var string $type
|
||||
* @var \stdClass[] $panels
|
||||
*/
|
||||
|
||||
$icons = '
|
||||
<div class="tracy-icons">
|
||||
<a href="#" data-tracy-action="window" title="open in window">¤</a>
|
||||
<a href="#" data-tracy-action="close" title="close window">×</a>
|
||||
</div>
|
||||
';
|
||||
|
||||
echo '<div itemscope>';
|
||||
|
||||
foreach ($panels as $panel) {
|
||||
$content = $panel->panel ? ($panel->panel . "\n" . $icons) : '';
|
||||
$class = 'tracy-panel ' . ($type === 'ajax' ? '' : 'tracy-panel-persist') . ' tracy-panel-' . $type; ?>
|
||||
<div class="<?= $class ?>" id="tracy-debug-panel-<?= $panel->id ?>" data-tracy-content='<?= str_replace(['&', "'"], ['&', '''], $content) ?>'></div><?php
|
||||
}
|
||||
|
||||
echo '<meta itemprop=tracy-snapshot content=', Dumper::formatSnapshotAttribute(Dumper::$liveSnapshot), '>';
|
||||
echo '</div>';
|
29
libs/Nette/Tracy/Bar/panels/dumps.panel.phtml
Normal file
29
libs/Nette/Tracy/Bar/panels/dumps.panel.phtml
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var array[] $data */
|
||||
?>
|
||||
<style class="tracy-debug">
|
||||
#tracy-debug .tracy-DumpPanel h2 {
|
||||
font: 11pt/1.5 sans-serif;
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
background: #3484d2;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Dumps</h1>
|
||||
|
||||
<div class="tracy-inner tracy-DumpPanel">
|
||||
<?php foreach ($data as $item): ?>
|
||||
<?php if ($item['title']):?>
|
||||
<h2><?= Helpers::escapeHtml($item['title']) ?></h2>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $item['dump'] ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
13
libs/Nette/Tracy/Bar/panels/dumps.tab.phtml
Normal file
13
libs/Nette/Tracy/Bar/panels/dumps.tab.phtml
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var array[] $data */
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<svg viewBox="0 0 2048 2048"><path fill="#154ABD" d="m1084 540c-110-1-228-2-325 58-54 35-87 94-126 143-94 162-71 383 59 519 83 94 207 151 333 149 132 3 261-60 344-160 122-138 139-355 44-511-73-66-133-158-234-183-31-9-65-9-95-14zm-60 116c73 0 53 115-16 97-105 5-195 102-192 207-2 78-122 48-95-23 8-153 151-285 304-280l-1-1zM1021 511"/><path fill="#4B6193" d="m1021 511c-284-2-560 131-746 344-53 64-118 125-145 206-16 86 59 152 103 217 219 267 575 428 921 377 312-44 600-241 755-515 39-81-30-156-74-217-145-187-355-327-581-384-77-19-156-29-234-28zm0 128c263-4 512 132 679 330 33 52 132 110 58 168-170 237-449 409-747 399-309 0-590-193-752-447 121-192 305-346 526-407 75-25 170-38 237-43z"/>
|
||||
</svg><span class="tracy-label">dumps</span>
|
21
libs/Nette/Tracy/Bar/panels/errors.panel.phtml
Normal file
21
libs/Nette/Tracy/Bar/panels/errors.panel.phtml
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var int[] $data */
|
||||
?>
|
||||
<h1>Errors</h1>
|
||||
|
||||
<div class="tracy-inner">
|
||||
<table class="tracy-sortable">
|
||||
<tr><th>Count</th><th>Error</th></tr>
|
||||
<?php foreach ($data as $item => $count): [$file, $line, $message] = explode('|', $item, 3) ?>
|
||||
<tr>
|
||||
<td class="tracy-right"><?= $count ? "$count\xC3\x97" : '' ?></td>
|
||||
<td><pre><?= Helpers::escapeHtml($message), ' in ', Helpers::editorLink($file, (int) $line) ?></pre></td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
25
libs/Nette/Tracy/Bar/panels/errors.tab.phtml
Normal file
25
libs/Nette/Tracy/Bar/panels/errors.tab.phtml
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var int[] $data */
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<style class="tracy-debug">
|
||||
#tracy-debug .tracy-ErrorTab {
|
||||
display: block;
|
||||
background: #D51616;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin: -1px -.4em;
|
||||
padding: 1px .4em;
|
||||
}
|
||||
</style>
|
||||
<span class="tracy-ErrorTab">
|
||||
<svg viewBox="0 0 2048 2048"><path fill="#fff" d="M1152 1503v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
|
||||
</svg><span class="tracy-label"><?= $sum = array_sum($data), $sum > 1 ? ' errors' : ' error' ?></span>
|
||||
</span>
|
125
libs/Nette/Tracy/Bar/panels/info.panel.phtml
Normal file
125
libs/Nette/Tracy/Bar/panels/info.panel.phtml
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var DefaultBarPanel $this */
|
||||
|
||||
if (isset($this->cpuUsage) && $this->time) {
|
||||
foreach (getrusage() as $key => $val) {
|
||||
$this->cpuUsage[$key] -= $val;
|
||||
}
|
||||
$userUsage = -round(($this->cpuUsage['ru_utime.tv_sec'] * 1e6 + $this->cpuUsage['ru_utime.tv_usec']) / $this->time / 10000);
|
||||
$systemUsage = -round(($this->cpuUsage['ru_stime.tv_sec'] * 1e6 + $this->cpuUsage['ru_stime.tv_usec']) / $this->time / 10000);
|
||||
}
|
||||
|
||||
$countClasses = function (array $list): int {
|
||||
return count(array_filter($list, function (string $name): bool {
|
||||
return (new \ReflectionClass($name))->isUserDefined();
|
||||
}));
|
||||
};
|
||||
|
||||
$ipFormatter = static function (?string $ip): ?string {
|
||||
if ($ip === '127.0.0.1' || $ip === '::1') {
|
||||
$ip .= ' (localhost)';
|
||||
}
|
||||
return $ip;
|
||||
};
|
||||
|
||||
$opcache = function_exists('opcache_get_status') ? @opcache_get_status() : null; // @ can be restricted
|
||||
$cachedFiles = isset($opcache['scripts']) ? array_intersect(array_keys($opcache['scripts']), get_included_files()) : [];
|
||||
$jit = $opcache['jit'] ?? null;
|
||||
|
||||
$info = [
|
||||
'Execution time' => number_format($this->time * 1000, 1, '.', "\u{202f}") . "\u{202f}ms",
|
||||
'CPU usage user + system' => isset($userUsage) ? (int) $userUsage . "\u{202f}% + " . (int) $systemUsage . "\u{202f}%" : null,
|
||||
'Peak of allocated memory' => number_format(memory_get_peak_usage() / 1000000, 2, '.', "\u{202f}") . "\u{202f}MB",
|
||||
'Included files' => count(get_included_files()),
|
||||
'Classes + interfaces + traits' => $countClasses(get_declared_classes()) . ' + '
|
||||
. $countClasses(get_declared_interfaces()) . ' + ' . $countClasses(get_declared_traits()),
|
||||
'OPcache' => $opcache ? round(count($cachedFiles) * 100 / count(get_included_files())) . "\u{202f}% cached" : '–',
|
||||
'JIT' => empty($jit['buffer_size']) ? '–' : round(($jit['buffer_size'] - $jit['buffer_free']) / $jit['buffer_size'] * 100) . "\u{202f}% used",
|
||||
'Your IP' => $ipFormatter($_SERVER['REMOTE_ADDR'] ?? null),
|
||||
'Server IP' => $ipFormatter($_SERVER['SERVER_ADDR'] ?? null),
|
||||
'HTTP method / response code' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] . ' / ' . http_response_code() : null,
|
||||
'PHP' => PHP_VERSION,
|
||||
'Xdebug' => extension_loaded('xdebug') ? phpversion('xdebug') : null,
|
||||
'Tracy' => Debugger::VERSION,
|
||||
'Server' => $_SERVER['SERVER_SOFTWARE'] ?? null,
|
||||
];
|
||||
|
||||
$info = array_map('strval', array_filter($info + (array) $this->data));
|
||||
|
||||
$packages = $devPackages = [];
|
||||
if (class_exists('Composer\Autoload\ClassLoader', false)) {
|
||||
$baseDir = (function () {
|
||||
@include dirname((new \ReflectionClass('Composer\Autoload\ClassLoader'))->getFileName()) . '/autoload_psr4.php'; // @ may not exist
|
||||
return $baseDir;
|
||||
})();
|
||||
$composer = @json_decode((string) file_get_contents($baseDir . '/composer.lock')); // @ may not exist or be valid
|
||||
[$packages, $devPackages] = [(array) @$composer->packages, (array) @$composer->{'packages-dev'}]; // @ keys may not exist
|
||||
foreach ([&$packages, &$devPackages] as &$items) {
|
||||
array_walk($items, function ($package) {
|
||||
$package->hash = $package->source->reference ?? $package->dist->reference ?? null;
|
||||
}, $items);
|
||||
usort($items, function ($a, $b): int { return $a->name <=> $b->name; });
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<style class="tracy-debug">
|
||||
#tracy-debug .tracy-InfoPanel td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#tracy-debug .tracy-InfoPanel td:nth-child(2) {
|
||||
font-weight: bold;
|
||||
width: 60%;
|
||||
}
|
||||
#tracy-debug .tracy-InfoPanel td[colspan='2'] b {
|
||||
float: right;
|
||||
margin-left: 2em;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>System info</h1>
|
||||
|
||||
<div class="tracy-inner tracy-InfoPanel">
|
||||
<div class="tracy-inner-container">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($info as $key => $val): ?>
|
||||
<tr>
|
||||
<?php if (strlen($val) > 25): ?>
|
||||
<td colspan=2><?= Helpers::escapeHtml($key) ?> <b><?= Helpers::escapeHtml($val) ?></b></td>
|
||||
<?php else: ?>
|
||||
<td><?= Helpers::escapeHtml($key) ?></td><td><?= Helpers::escapeHtml($val) ?></td>
|
||||
<?php endif ?>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
|
||||
<?php if ($packages || $devPackages): ?>
|
||||
<h2><a class="tracy-toggle tracy-collapsed" data-tracy-ref="^div .tracy-InfoPanel-packages">Composer Packages (<?= count($packages), $devPackages ? ' + ' . count($devPackages) . ' dev' : '' ?>)</a></h2>
|
||||
|
||||
<div class="tracy-InfoPanel-packages tracy-collapsed">
|
||||
<?php if ($packages): ?>
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($packages as $package): ?>
|
||||
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($devPackages): ?>
|
||||
<h2>Dev Packages</h2>
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($devPackages as $package): ?>
|
||||
<tr><td><?= Helpers::escapeHtml($package->name) ?></td><td><?= Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
15
libs/Nette/Tracy/Bar/panels/info.tab.phtml
Normal file
15
libs/Nette/Tracy/Bar/panels/info.tab.phtml
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/** @var DefaultBarPanel $this */
|
||||
|
||||
$this->time = microtime(true) - Debugger::$time;
|
||||
|
||||
?>
|
||||
<span title="Execution time">
|
||||
<svg viewBox="0 0 2048 2048"><path fill="#86bbf0" d="m640 1153.6v639.3h-256v-639.3z"/><path fill="#6ba9e6" d="m1024 254.68v1538.2h-256v-1538.2z"/><path fill="#4f96dc" d="m1408 897.57v894.3h-256v-894.3z"/><path fill="#3987d4" d="m1792 513.08v1279.8h-256v-1279.8z"/>
|
||||
</svg><span class="tracy-label"><?= number_format($this->time * 1000, 1, '.', "\u{202f}") ?> ms</span>
|
||||
</span>
|
626
libs/Nette/Tracy/BlueScreen/BlueScreen.php
Normal file
626
libs/Nette/Tracy/BlueScreen/BlueScreen.php
Normal file
@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* Red BlueScreen.
|
||||
*/
|
||||
class BlueScreen
|
||||
{
|
||||
private const MaxMessageLength = 2000;
|
||||
|
||||
/** @var string[] */
|
||||
public $info = [];
|
||||
|
||||
/** @var string[] paths to be collapsed in stack trace (e.g. core libraries) */
|
||||
public $collapsePaths = [];
|
||||
|
||||
/** @var int */
|
||||
public $maxDepth = 5;
|
||||
|
||||
/** @var int */
|
||||
public $maxLength = 150;
|
||||
|
||||
/** @var int */
|
||||
public $maxItems = 100;
|
||||
|
||||
/** @var callable|null a callable returning true for sensitive data; fn(string $key, mixed $val): bool */
|
||||
public $scrubber;
|
||||
|
||||
/** @var string[] */
|
||||
public $keysToHide = [
|
||||
'password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin', 'authorization',
|
||||
self::class . '::$snapshot',
|
||||
];
|
||||
|
||||
/** @var bool */
|
||||
public $showEnvironment = true;
|
||||
|
||||
/** @var callable[] */
|
||||
private $panels = [];
|
||||
|
||||
/** @var callable[] functions that returns action for exceptions */
|
||||
private $actions = [];
|
||||
|
||||
/** @var callable[] */
|
||||
private $fileGenerators = [];
|
||||
|
||||
/** @var array */
|
||||
private $snapshot;
|
||||
|
||||
/** @var \WeakMap<\Fiber|\Generator> */
|
||||
private $fibers;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->collapsePaths = preg_match('#(.+/vendor)/tracy/tracy/src/Tracy/BlueScreen$#', strtr(__DIR__, '\\', '/'), $m)
|
||||
? [$m[1] . '/tracy', $m[1] . '/nette', $m[1] . '/latte']
|
||||
: [dirname(__DIR__)];
|
||||
$this->fileGenerators[] = [self::class, 'generateNewPhpFileContents'];
|
||||
$this->fibers = PHP_VERSION_ID < 80000 ? new \SplObjectStorage : new \WeakMap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add custom panel as function (?\Throwable $e): ?array
|
||||
* @return static
|
||||
*/
|
||||
public function addPanel(callable $panel): self
|
||||
{
|
||||
if (!in_array($panel, $this->panels, true)) {
|
||||
$this->panels[] = $panel;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add action.
|
||||
* @return static
|
||||
*/
|
||||
public function addAction(callable $action): self
|
||||
{
|
||||
$this->actions[] = $action;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add new file generator.
|
||||
* @param callable(string): ?string $generator
|
||||
* @return static
|
||||
*/
|
||||
public function addFileGenerator(callable $generator): self
|
||||
{
|
||||
$this->fileGenerators[] = $generator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \Fiber|\Generator $fiber
|
||||
* @return static
|
||||
*/
|
||||
public function addFiber($fiber): self
|
||||
{
|
||||
$this->fibers[$fiber] = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders blue screen.
|
||||
*/
|
||||
public function render(\Throwable $exception): void
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
}
|
||||
|
||||
$this->renderTemplate($exception, __DIR__ . '/assets/page.phtml');
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public function renderToAjax(\Throwable $exception, DeferredContent $defer): void
|
||||
{
|
||||
$defer->addSetup('Tracy.BlueScreen.loadAjax', Helpers::capture(function () use ($exception) {
|
||||
$this->renderTemplate($exception, __DIR__ . '/assets/content.phtml');
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders blue screen to file (if file exists, it will not be overwritten).
|
||||
*/
|
||||
public function renderToFile(\Throwable $exception, string $file): bool
|
||||
{
|
||||
if ($handle = @fopen($file, 'x')) {
|
||||
ob_start(); // double buffer prevents sending HTTP headers in some PHP
|
||||
ob_start(function ($buffer) use ($handle): void { fwrite($handle, $buffer); }, 4096);
|
||||
$this->renderTemplate($exception, __DIR__ . '/assets/page.phtml', false);
|
||||
ob_end_flush();
|
||||
ob_end_clean();
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private function renderTemplate(\Throwable $exception, string $template, $toScreen = true): void
|
||||
{
|
||||
[$generators, $fibers] = $this->findGeneratorsAndFibers($exception);
|
||||
$headersSent = headers_sent($headersFile, $headersLine);
|
||||
$obStatus = Debugger::$obStatus;
|
||||
$showEnvironment = $this->showEnvironment && (strpos($exception->getMessage(), 'Allowed memory size') === false);
|
||||
$info = array_filter($this->info);
|
||||
$source = Helpers::getSource();
|
||||
$title = $exception instanceof \ErrorException
|
||||
? Helpers::errorTypeToString($exception->getSeverity())
|
||||
: Helpers::getClass($exception);
|
||||
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error
|
||||
? null
|
||||
: error_get_last();
|
||||
|
||||
if (function_exists('apache_request_headers')) {
|
||||
$httpHeaders = apache_request_headers();
|
||||
} else {
|
||||
$httpHeaders = array_filter($_SERVER, function ($k) { return strncmp($k, 'HTTP_', 5) === 0; }, ARRAY_FILTER_USE_KEY);
|
||||
$httpHeaders = array_combine(array_map(function ($k) { return strtolower(strtr(substr($k, 5), '_', '-')); }, array_keys($httpHeaders)), $httpHeaders);
|
||||
}
|
||||
|
||||
$snapshot = &$this->snapshot;
|
||||
$snapshot = [];
|
||||
$dump = $this->getDumper();
|
||||
|
||||
$css = array_map('file_get_contents', array_merge([
|
||||
__DIR__ . '/../assets/reset.css',
|
||||
__DIR__ . '/assets/bluescreen.css',
|
||||
__DIR__ . '/../assets/toggle.css',
|
||||
__DIR__ . '/../assets/table-sort.css',
|
||||
__DIR__ . '/../assets/tabs.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper-light.css',
|
||||
], Debugger::$customCssFiles));
|
||||
$css = Helpers::minifyCss(implode('', $css));
|
||||
|
||||
$nonce = $toScreen ? Helpers::getNonce() : null;
|
||||
$actions = $toScreen ? $this->renderActions($exception) : [];
|
||||
|
||||
require $template;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return \stdClass[]
|
||||
*/
|
||||
private function renderPanels(?\Throwable $ex): array
|
||||
{
|
||||
$obLevel = ob_get_level();
|
||||
$res = [];
|
||||
foreach ($this->panels as $callback) {
|
||||
try {
|
||||
$panel = $callback($ex);
|
||||
if (empty($panel['tab']) || empty($panel['panel'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$res[] = (object) $panel;
|
||||
continue;
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
while (ob_get_level() > $obLevel) { // restore ob-level if broken
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
is_callable($callback, true, $name);
|
||||
$res[] = (object) [
|
||||
'tab' => "Error in panel $name",
|
||||
'panel' => nl2br(Helpers::escapeHtml($e)),
|
||||
];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
private function renderActions(\Throwable $ex): array
|
||||
{
|
||||
$actions = [];
|
||||
foreach ($this->actions as $callback) {
|
||||
$action = $callback($ex);
|
||||
if (!empty($action['link']) && !empty($action['label'])) {
|
||||
$actions[] = $action;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
property_exists($ex, 'tracyAction')
|
||||
&& !empty($ex->tracyAction['link'])
|
||||
&& !empty($ex->tracyAction['label'])
|
||||
) {
|
||||
$actions[] = $ex->tracyAction;
|
||||
}
|
||||
|
||||
if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{3,})+)\1#i', $ex->getMessage(), $m)) {
|
||||
$class = $m[2];
|
||||
if (
|
||||
!class_exists($class, false) && !interface_exists($class, false) && !trait_exists($class, false)
|
||||
&& ($file = Helpers::guessClassFile($class)) && !@is_file($file) // @ - may trigger error
|
||||
) {
|
||||
[$content, $line] = $this->generateNewFileContents($file, $class);
|
||||
$actions[] = [
|
||||
'link' => Helpers::editorUri($file, $line, 'create', '', $content),
|
||||
'label' => 'create class',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('# ([\'"])((?:/|[a-z]:[/\\\\])\w[^\'"]+\.\w{2,5})\1#i', $ex->getMessage(), $m)) {
|
||||
$file = $m[2];
|
||||
if (@is_file($file)) { // @ - may trigger error
|
||||
$label = 'open';
|
||||
$content = '';
|
||||
$line = 1;
|
||||
} else {
|
||||
$label = 'create';
|
||||
[$content, $line] = $this->generateNewFileContents($file);
|
||||
}
|
||||
|
||||
$actions[] = [
|
||||
'link' => Helpers::editorUri($file, $line, $label, '', $content),
|
||||
'label' => $label . ' file',
|
||||
];
|
||||
}
|
||||
|
||||
$query = ($ex instanceof \ErrorException ? '' : Helpers::getClass($ex) . ' ')
|
||||
. preg_replace('#\'.*\'|".*"#Us', '', $ex->getMessage());
|
||||
$actions[] = [
|
||||
'link' => 'https://www.google.com/search?sourceid=tracy&q=' . urlencode($query),
|
||||
'label' => 'search',
|
||||
'external' => true,
|
||||
];
|
||||
|
||||
if (
|
||||
$ex instanceof \ErrorException
|
||||
&& !empty($ex->skippable)
|
||||
&& preg_match('#^https?://#', $source = Helpers::getSource())
|
||||
) {
|
||||
$actions[] = [
|
||||
'link' => $source . (strpos($source, '?') ? '&' : '?') . '_tracy_skip_error',
|
||||
'label' => 'skip error',
|
||||
];
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns syntax highlighted source code.
|
||||
*/
|
||||
public static function highlightFile(
|
||||
string $file,
|
||||
int $line,
|
||||
int $lines = 15,
|
||||
bool $php = true,
|
||||
int $column = 0
|
||||
): ?string
|
||||
{
|
||||
$source = @file_get_contents($file); // @ file may not exist
|
||||
if ($source === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$source = $php
|
||||
? static::highlightPhp($source, $line, $lines, $column)
|
||||
: '<pre class=tracy-code><div>' . static::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $lines, $column) . '</div></pre>';
|
||||
|
||||
if ($editor = Helpers::editorUri($file, $line)) {
|
||||
$source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns syntax highlighted source code.
|
||||
*/
|
||||
public static function highlightPhp(string $source, int $line, int $lines = 15, int $column = 0): string
|
||||
{
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('highlight.comment', '#998; font-style: italic');
|
||||
ini_set('highlight.default', '#000');
|
||||
ini_set('highlight.html', '#06B');
|
||||
ini_set('highlight.keyword', '#D24; font-weight: bold');
|
||||
ini_set('highlight.string', '#080');
|
||||
}
|
||||
|
||||
$source = preg_replace('#(__halt_compiler\s*\(\)\s*;).*#is', '$1', $source);
|
||||
$source = str_replace(["\r\n", "\r"], "\n", $source);
|
||||
$source = explode("\n", highlight_string($source, true));
|
||||
$out = $source[0]; // <code><span color=highlight.html>
|
||||
$source = str_replace('<br />', "\n", $source[1]);
|
||||
$out .= static::highlightLine($source, $line, $lines, $column);
|
||||
$out = str_replace(' ', ' ', $out) . '</code>';
|
||||
return "<pre class='tracy-code'><div>$out</div></pre>";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns highlighted line in HTML code.
|
||||
*/
|
||||
public static function highlightLine(string $html, int $line, int $lines = 15, int $column = 0): string
|
||||
{
|
||||
$source = explode("\n", "\n" . str_replace("\r\n", "\n", $html));
|
||||
$out = '';
|
||||
$spans = 1;
|
||||
$start = $i = max(1, min($line, count($source) - 1) - (int) floor($lines * 2 / 3));
|
||||
while (--$i >= 1) { // find last highlighted block
|
||||
if (preg_match('#.*(</?span[^>]*>)#', $source[$i], $m)) {
|
||||
if ($m[1] !== '</span>') {
|
||||
$spans++;
|
||||
$out .= $m[1];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$source = array_slice($source, $start, $lines, true);
|
||||
end($source);
|
||||
$numWidth = strlen((string) key($source));
|
||||
|
||||
foreach ($source as $n => $s) {
|
||||
$spans += substr_count($s, '<span') - substr_count($s, '</span');
|
||||
$s = str_replace(["\r", "\n"], ['', ''], $s);
|
||||
preg_match_all('#<[^>]+>#', $s, $tags);
|
||||
if ($n == $line) {
|
||||
$s = strip_tags($s);
|
||||
if ($column) {
|
||||
$s = preg_replace(
|
||||
'#((?:&.*?;|[^&]){' . ($column - 1) . '})(&.*?;|.)#u',
|
||||
'\1<span class="tracy-column-highlight">\2</span>',
|
||||
$s . ' ',
|
||||
1
|
||||
);
|
||||
}
|
||||
$out .= sprintf(
|
||||
"<span class='tracy-line-highlight'>%{$numWidth}s: %s\n</span>%s",
|
||||
$n,
|
||||
$s,
|
||||
implode('', $tags[0])
|
||||
);
|
||||
} else {
|
||||
$out .= sprintf("<span class='tracy-line'>%{$numWidth}s:</span> %s\n", $n, $s);
|
||||
}
|
||||
}
|
||||
|
||||
$out .= str_repeat('</span>', $spans);
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns syntax highlighted source code to Terminal.
|
||||
*/
|
||||
public static function highlightPhpCli(string $file, int $line, int $lines = 15, int $column = 0): ?string
|
||||
{
|
||||
$source = @file_get_contents($file); // @ file may not exist
|
||||
if ($source === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$s = self::highlightPhp($source, $line, $lines);
|
||||
|
||||
$colors = [
|
||||
'color: ' . ini_get('highlight.comment') => '1;30',
|
||||
'color: ' . ini_get('highlight.default') => '1;36',
|
||||
'color: ' . ini_get('highlight.html') => '1;35',
|
||||
'color: ' . ini_get('highlight.keyword') => '1;37',
|
||||
'color: ' . ini_get('highlight.string') => '1;32',
|
||||
'tracy-line' => '1;30',
|
||||
'tracy-line-highlight' => "1;37m\e[41",
|
||||
];
|
||||
|
||||
$stack = ['0'];
|
||||
$s = preg_replace_callback(
|
||||
'#<\w+(?: (class|style)=["\'](.*?)["\'])?[^>]*>|</\w+>#',
|
||||
function ($m) use ($colors, &$stack): string {
|
||||
if ($m[0][1] === '/') {
|
||||
array_pop($stack);
|
||||
} else {
|
||||
$stack[] = isset($m[2], $colors[$m[2]]) ? $colors[$m[2]] : '0';
|
||||
}
|
||||
|
||||
return "\e[0m\e[" . end($stack) . 'm';
|
||||
},
|
||||
$s
|
||||
);
|
||||
$s = htmlspecialchars_decode(strip_tags($s), ENT_QUOTES | ENT_HTML5);
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Should a file be collapsed in stack trace?
|
||||
* @internal
|
||||
*/
|
||||
public function isCollapsed(string $file): bool
|
||||
{
|
||||
$file = strtr($file, '\\', '/') . '/';
|
||||
foreach ($this->collapsePaths as $path) {
|
||||
$path = strtr($path, '\\', '/') . '/';
|
||||
if (strncmp($file, $path, strlen($path)) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public function getDumper(): \Closure
|
||||
{
|
||||
return function ($v, $k = null): string {
|
||||
return Dumper::toHtml($v, [
|
||||
Dumper::DEPTH => $this->maxDepth,
|
||||
Dumper::TRUNCATE => $this->maxLength,
|
||||
Dumper::ITEMS => $this->maxItems,
|
||||
Dumper::SNAPSHOT => &$this->snapshot,
|
||||
Dumper::LOCATION => Dumper::LOCATION_CLASS,
|
||||
Dumper::SCRUBBER => $this->scrubber,
|
||||
Dumper::KEYS_TO_HIDE => $this->keysToHide,
|
||||
], $k);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public function formatMessage(\Throwable $exception): string
|
||||
{
|
||||
$msg = Helpers::encodeString(trim((string) $exception->getMessage()), self::MaxMessageLength, false);
|
||||
|
||||
// highlight 'string'
|
||||
$msg = preg_replace(
|
||||
'#\'\S(?:[^\']|\\\\\')*\S\'|"\S(?:[^"]|\\\\")*\S"#',
|
||||
'<i>$0</i>',
|
||||
$msg
|
||||
);
|
||||
|
||||
// clickable class & methods
|
||||
$msg = preg_replace_callback(
|
||||
'#(\w+\\\\[\w\\\\]+\w)(?:::(\w+))?#',
|
||||
function ($m) {
|
||||
if (isset($m[2]) && method_exists($m[1], $m[2])) {
|
||||
$r = new \ReflectionMethod($m[1], $m[2]);
|
||||
} elseif (class_exists($m[1], false) || interface_exists($m[1], false)) {
|
||||
$r = new \ReflectionClass($m[1]);
|
||||
}
|
||||
|
||||
if (empty($r) || !$r->getFileName()) {
|
||||
return $m[0];
|
||||
}
|
||||
|
||||
return '<a href="' . Helpers::escapeHtml(Helpers::editorUri($r->getFileName(), $r->getStartLine())) . '" class="tracy-editor">' . $m[0] . '</a>';
|
||||
},
|
||||
$msg
|
||||
);
|
||||
|
||||
// clickable file name
|
||||
$msg = preg_replace_callback(
|
||||
'#([\w\\\\/.:-]+\.(?:php|phpt|phtml|latte|neon))(?|:(\d+)| on line (\d+))?#',
|
||||
function ($m) {
|
||||
return @is_file($m[1])
|
||||
? '<a href="' . Helpers::escapeHtml(Helpers::editorUri($m[1], isset($m[2]) ? (int) $m[2] : null)) . '" class="tracy-editor">' . $m[0] . '</a>'
|
||||
: $m[0];
|
||||
},
|
||||
$msg
|
||||
);
|
||||
|
||||
return $msg;
|
||||
}
|
||||
|
||||
|
||||
private function renderPhpInfo(): void
|
||||
{
|
||||
ob_start();
|
||||
@phpinfo(INFO_LICENSE); // @ phpinfo may be disabled
|
||||
$license = ob_get_clean();
|
||||
ob_start();
|
||||
@phpinfo(INFO_CONFIGURATION | INFO_MODULES); // @ phpinfo may be disabled
|
||||
$info = ob_get_clean();
|
||||
|
||||
if (strpos($license, '<body') === false) {
|
||||
echo '<pre class="tracy-dump tracy-light">', Helpers::escapeHtml($info), '</pre>';
|
||||
} else {
|
||||
$info = str_replace('<table', '<table class="tracy-sortable"', $info);
|
||||
echo preg_replace('#^.+<body>|</body>.+\z|<hr />|<h1>Configuration</h1>#s', '', $info);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
private function generateNewFileContents(string $file, ?string $class = null): array
|
||||
{
|
||||
foreach (array_reverse($this->fileGenerators) as $generator) {
|
||||
$content = $generator($file, $class);
|
||||
if ($content !== null) {
|
||||
$line = 1;
|
||||
$pos = strpos($content, '$END$');
|
||||
if ($pos !== false) {
|
||||
$content = substr_replace($content, '', $pos, 5);
|
||||
$line = substr_count($content, "\n", 0, $pos) + 1;
|
||||
}
|
||||
|
||||
return [$content, $line];
|
||||
}
|
||||
}
|
||||
|
||||
return ['', 1];
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function generateNewPhpFileContents(string $file, ?string $class = null): ?string
|
||||
{
|
||||
if (substr($file, -4) !== '.php') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$res = "<?php\n\ndeclare(strict_types=1);\n\n";
|
||||
if (!$class) {
|
||||
return $res . '$END$';
|
||||
}
|
||||
|
||||
if ($pos = strrpos($class, '\\')) {
|
||||
$res .= 'namespace ' . substr($class, 0, $pos) . ";\n\n";
|
||||
$class = substr($class, $pos + 1);
|
||||
}
|
||||
|
||||
return $res . "class $class\n{\n\$END\$\n}\n";
|
||||
}
|
||||
|
||||
|
||||
private function findGeneratorsAndFibers(object $object): array
|
||||
{
|
||||
$generators = $fibers = [];
|
||||
$add = function ($obj) use (&$generators, &$fibers) {
|
||||
if ($obj instanceof \Generator) {
|
||||
try {
|
||||
new \ReflectionGenerator($obj);
|
||||
$generators[spl_object_id($obj)] = $obj;
|
||||
} catch (\ReflectionException $e) {
|
||||
}
|
||||
} elseif ($obj instanceof \Fiber && $obj->isStarted() && !$obj->isTerminated()) {
|
||||
$fibers[spl_object_id($obj)] = $obj;
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($this->fibers as $k => $v) {
|
||||
$add($this->fibers instanceof \WeakMap ? $k : $v);
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
Helpers::traverseValue($object, $add);
|
||||
}
|
||||
|
||||
return [$generators, $fibers];
|
||||
}
|
||||
}
|
418
libs/Nette/Tracy/BlueScreen/assets/bluescreen.css
Normal file
418
libs/Nette/Tracy/BlueScreen/assets/bluescreen.css
Normal file
@ -0,0 +1,418 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
:root {
|
||||
--tracy-space: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:root {
|
||||
--tracy-space: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
html.tracy-bs-visible,
|
||||
html.tracy-bs-visible body {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#tracy-bs {
|
||||
font: 9pt/1.5 Verdana, sans-serif;
|
||||
background: white;
|
||||
color: #333;
|
||||
position: absolute;
|
||||
z-index: 20000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#tracy-bs a {
|
||||
text-decoration: none;
|
||||
color: #328ADC;
|
||||
padding: 0 4px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
|
||||
#tracy-bs a + a {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#tracy-bs a:hover,
|
||||
#tracy-bs a:focus {
|
||||
color: #085AA3;
|
||||
}
|
||||
|
||||
#tracy-bs-toggle {
|
||||
position: absolute;
|
||||
right: .5em;
|
||||
top: .5em;
|
||||
text-decoration: none;
|
||||
background: #CD1818;
|
||||
color: white !important;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#tracy-bs-toggle.tracy-collapsed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.tracy-bs-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 80vh;
|
||||
}
|
||||
|
||||
.tracy-bs-main.tracy-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracy-bs p,
|
||||
#tracy-bs table,
|
||||
#tracy-bs pre,
|
||||
#tracy-bs h1,
|
||||
#tracy-bs h2,
|
||||
#tracy-bs h3 {
|
||||
margin: 0 0 var(--tracy-space);
|
||||
}
|
||||
|
||||
#tracy-bs h1 {
|
||||
font-size: 15pt;
|
||||
font-weight: normal;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
#tracy-bs h1 span {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#tracy-bs h2 {
|
||||
font-size: 14pt;
|
||||
font-weight: normal;
|
||||
margin-top: var(--tracy-space);
|
||||
}
|
||||
|
||||
#tracy-bs h3 {
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tracy-bs pre,
|
||||
#tracy-bs code,
|
||||
#tracy-bs table {
|
||||
font: 9pt/1.5 Consolas, monospace !important;
|
||||
}
|
||||
|
||||
#tracy-bs pre,
|
||||
#tracy-bs table {
|
||||
background: #FDF5CE;
|
||||
padding: .4em .7em;
|
||||
border: 2px solid #ffffffa6;
|
||||
box-shadow: 1px 2px 6px #00000005;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#tracy-bs table pre {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#tracy-bs table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tracy-bs td,
|
||||
#tracy-bs th {
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #e6dfbf;
|
||||
}
|
||||
|
||||
#tracy-bs th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tracy-bs tr > :first-child {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#tracy-bs tr:nth-child(2n),
|
||||
#tracy-bs tr:nth-child(2n) pre {
|
||||
background-color: #F7F0CB;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-footer--sticky {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#tracy-bs footer ul {
|
||||
font-size: 7pt;
|
||||
padding: var(--tracy-space);
|
||||
margin: var(--tracy-space) 0 0;
|
||||
color: #777;
|
||||
background: #F6F5F3;
|
||||
border-top: 1px solid #DDD;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-footer-logo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-footer-logo a {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAUBAMAAAD/1DctAAAAMFBMVEWupZzj39rEvbTy8O3X0sz9/PvGwLu8tavQysHq6OS0rKP5+Pbd2dT29fPMxbzPx8DKErMJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGUlEQVQoFX3TQWgTQRQA0MWLIJJDYehBTykhG5ERTx56K1u8eEhCYtomE7x5L4iLh0ViF7egewuFFqSIYE6hIHsIYQ6CQSg9CDKn4QsNCRlB59C74J/ZNHW1+An5+bOPyf6/s46oz2P+A0yIeZZ2ieEHi6TOnLKTxvWq+b52mxlVO3xnM1s7xLX1504XQH65OnW2dBqn7cCkYsFsfYsWpyY/2salmFTpEyzeR8zosYqMdiPDXdyU52K1wgEa/SjGpdEwUAxqvRfckQCDOyFearsEHe2grvkh/cFAHKvdtI3lcVceKQIOFpv+FOZaNPQBwJZLPp+hfrvT5JZXaUFsR8zqQc9qSgAharkfS5M/5F6nGJJAtXq/eLr3ucZpHccSxOOIPaQhtHohpCH2Xu6rLmQ0djnr4/+J3C6v+AW8/XWYxwYNdlhWj/P5fPSTQwVr0T9lGxdaBCqErNZaqYnEwbkjEB3NasGF3lPdrHa1nnxNOMgj0+neePUPjd2v/qVvUv29ifvc19huQ48qwXShy/9o8o3OSk0cs37mOFd0Ydgvsf/oZEnPVtggfd66lORn9mDyyzXU13SRtH2L6aR5T/snGAcZPfAXz5J1YlJWBEuxdMYqQecpBrlM49xAbmqyHA+xlA1FxBtqT2xmJoNXZlIt74ZBLeJ9ZGDqByNI7p543idzJ23vXEv7IgnsxiS+eNtwNbFdLq7+Bi4wQ0I4SVb9AAAAAElFTkSuQmCC') no-repeat;
|
||||
opacity: .6;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-footer-logo a:hover,
|
||||
#tracy-bs .tracy-footer-logo a:focus {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
|
||||
#tracy-bs .tracy-section {
|
||||
padding-left: calc(1.5 * var(--tracy-space));
|
||||
padding-right: calc(1.5 * var(--tracy-space));
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section-panel {
|
||||
background: #F4F3F1;
|
||||
padding: var(--tracy-space) var(--tracy-space) 0;
|
||||
margin: 0 0 var(--tracy-space);
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 1px 1px 0px 0 #00000005;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tracy-bs .outer, /* deprecated */
|
||||
#tracy-bs .tracy-pane {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#tracy-bs.tracy-mac .tracy-pane {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
/* header */
|
||||
#tracy-bs .tracy-section--error {
|
||||
background: #CD1818;
|
||||
color: white;
|
||||
font-size: 13pt;
|
||||
padding-top: var(--tracy-space);
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error::selection,
|
||||
#tracy-bs .tracy-section--error ::selection {
|
||||
color: black !important;
|
||||
background: #FDF5CE !important;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error a {
|
||||
color: #ffefa1 !important;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error span span {
|
||||
font-size: 80%;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error a.tracy-action {
|
||||
color: white !important;
|
||||
opacity: 0;
|
||||
font-size: .7em;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error:hover a.tracy-action {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error a.tracy-action:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error i {
|
||||
color: #ffefa1;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
/* source code */
|
||||
#tracy-bs pre.tracy-code > div {
|
||||
min-width: 100%;
|
||||
float: left;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-line-highlight {
|
||||
background: #CD1818;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
padding: 0 1ch;
|
||||
margin: 0 -1ch;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-column-highlight {
|
||||
display: inline-block;
|
||||
backdrop-filter: grayscale(1);
|
||||
margin: 0 -1px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-line {
|
||||
color: #9F9C7F;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#tracy-bs a.tracy-editor {
|
||||
color: inherit;
|
||||
border-bottom: 1px dotted rgba(0, 0, 0, .3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#tracy-bs a.tracy-editor:hover {
|
||||
background: #0001;
|
||||
}
|
||||
|
||||
#tracy-bs span[data-tracy-href] {
|
||||
border-bottom: 1px dotted rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-dump-whitespace {
|
||||
color: #0003;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-caused {
|
||||
float: right;
|
||||
padding: .3em calc(1.5 * var(--tracy-space));
|
||||
background: #df8075;
|
||||
border-radius: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-caused a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
margin-bottom: calc(.5 * var(--tracy-space));
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack-file {
|
||||
text-align: right;
|
||||
padding-right: var(--tracy-space);
|
||||
white-space: nowrap;
|
||||
height: calc(1.5 * var(--tracy-space));
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack-callee {
|
||||
white-space: nowrap;
|
||||
height: calc(1.5 * var(--tracy-space));
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack-additional {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack-args tr:first-child > * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-callstack-args tr:first-child td:before {
|
||||
position: absolute;
|
||||
right: .3em;
|
||||
content: 'may not be true';
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-panel-fadein {
|
||||
animation: tracy-panel-fadein .12s ease;
|
||||
}
|
||||
|
||||
@keyframes tracy-panel-fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--causedby {
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--causedby:not(.tracy-collapsed) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--causedby .tracy-section--error {
|
||||
background: #cd1818a6;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-section--error + .tracy-section--stack {
|
||||
margin-top: calc(1.5 * var(--tracy-space));
|
||||
}
|
||||
|
||||
|
||||
/* tabs */
|
||||
#tracy-bs .tracy-tab-bar {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-tab-bar > *:not(:first-child) {
|
||||
margin-left: var(--tracy-space);
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-tab-bar a {
|
||||
display: block;
|
||||
padding: calc(.5 * var(--tracy-space)) var(--tracy-space);
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px 5px 0 0;
|
||||
text-decoration: none;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-tab-bar > .tracy-active a {
|
||||
background: white;
|
||||
}
|
||||
|
||||
#tracy-bs .tracy-tab-panel {
|
||||
border-top: 2px solid white;
|
||||
padding-top: var(--tracy-space);
|
||||
overflow: auto;
|
||||
}
|
77
libs/Nette/Tracy/BlueScreen/assets/bluescreen.js
Normal file
77
libs/Nette/Tracy/BlueScreen/assets/bluescreen.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
class BlueScreen
|
||||
{
|
||||
static init(ajax) {
|
||||
BlueScreen.globalInit();
|
||||
|
||||
let blueScreen = document.getElementById('tracy-bs');
|
||||
|
||||
document.documentElement.classList.add('tracy-bs-visible');
|
||||
if (navigator.platform.indexOf('Mac') > -1) {
|
||||
blueScreen.classList.add('tracy-mac');
|
||||
}
|
||||
|
||||
blueScreen.addEventListener('tracy-toggle', (e) => {
|
||||
if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle
|
||||
document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed);
|
||||
|
||||
} else if (!e.target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle
|
||||
e.detail.relatedTarget.classList.toggle('tracy-panel-fadein', !e.detail.collapsed);
|
||||
}
|
||||
});
|
||||
|
||||
if (!ajax) {
|
||||
document.body.appendChild(blueScreen);
|
||||
let id = location.href + document.querySelector('.tracy-section--error').textContent;
|
||||
Tracy.Toggle.persist(blueScreen, sessionStorage.getItem('tracy-toggles-bskey') === id);
|
||||
sessionStorage.setItem('tracy-toggles-bskey', id);
|
||||
}
|
||||
|
||||
(new ResizeObserver(stickyFooter)).observe(blueScreen);
|
||||
|
||||
if (document.documentElement.classList.contains('tracy-bs-visible')) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static globalInit() {
|
||||
// enables toggling via ESC
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.keyCode === 27 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { // ESC
|
||||
Tracy.Toggle.toggle(document.getElementById('tracy-bs-toggle'));
|
||||
}
|
||||
});
|
||||
|
||||
Tracy.TableSort.init();
|
||||
Tracy.Tabs.init();
|
||||
|
||||
window.addEventListener('scroll', stickyFooter);
|
||||
|
||||
BlueScreen.globalInit = function() {};
|
||||
}
|
||||
|
||||
|
||||
static loadAjax(content) {
|
||||
let ajaxBs = document.getElementById('tracy-bs');
|
||||
if (ajaxBs) {
|
||||
ajaxBs.remove();
|
||||
}
|
||||
document.body.insertAdjacentHTML('beforeend', content);
|
||||
ajaxBs = document.getElementById('tracy-bs');
|
||||
Tracy.Dumper.init(ajaxBs);
|
||||
BlueScreen.init(true);
|
||||
}
|
||||
}
|
||||
|
||||
function stickyFooter() {
|
||||
let footer = document.querySelector('#tracy-bs footer');
|
||||
footer.classList.toggle('tracy-footer--sticky', false); // to measure footer.offsetTop
|
||||
footer.classList.toggle('tracy-footer--sticky', footer.offsetHeight + footer.offsetTop - window.innerHeight - document.documentElement.scrollTop < 0);
|
||||
}
|
||||
|
||||
let Tracy = window.Tracy = window.Tracy || {};
|
||||
Tracy.BlueScreen = Tracy.BlueScreen || BlueScreen;
|
73
libs/Nette/Tracy/BlueScreen/assets/content.phtml
Normal file
73
libs/Nette/Tracy/BlueScreen/assets/content.phtml
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $exception
|
||||
* @var array[] $actions
|
||||
* @var string[] $info
|
||||
* @var string $source
|
||||
* @var ?array $lastError
|
||||
* @var string[] $httpHeaders
|
||||
* @var callable $dump
|
||||
* @var array $snapshot
|
||||
* @var bool $showEnvironment
|
||||
* @var BlueScreen $this
|
||||
* @var bool $headersSent
|
||||
* @var ?string $headersFile
|
||||
* @var ?int $headersLine
|
||||
* @var ?array $obStatus
|
||||
* @var \Generator[] $generators
|
||||
* @var \Fiber[] $fibers
|
||||
*/
|
||||
?>
|
||||
<tracy-div id="tracy-bs" itemscope>
|
||||
<a id="tracy-bs-toggle" href="#" class="tracy-toggle"></a>
|
||||
<div class="tracy-bs-main">
|
||||
<?php $ex = $exception; $exceptions = []; ?>
|
||||
<?php require __DIR__ . '/section-exception.phtml' ?>
|
||||
|
||||
<?php require __DIR__ . '/section-lastMutedError.phtml' ?>
|
||||
|
||||
<?php $bottomPanels = [] ?>
|
||||
<?php foreach ($this->renderPanels(null) as $panel): ?>
|
||||
<?php if (!empty($panel->bottom)) { $bottomPanels[] = $panel; continue; } ?>
|
||||
<?php $collapsedClass = !isset($panel->collapsed) || $panel->collapsed ? ' tracy-collapsed' : ''; ?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= $collapsedClass ?>"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
|
||||
|
||||
<div class="tracy-section-panel<?= $collapsedClass ?>">
|
||||
<?= $panel->panel ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php require __DIR__ . '/section-environment.phtml' ?>
|
||||
|
||||
<?php require __DIR__ . '/section-cli.phtml' ?>
|
||||
|
||||
<?php require __DIR__ . '/section-http.phtml' ?>
|
||||
|
||||
<?php foreach ($bottomPanels as $panel): ?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
|
||||
|
||||
<div class="tracy-section-panel">
|
||||
<?= $panel->panel ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach ?>
|
||||
|
||||
<footer>
|
||||
<ul>
|
||||
<li><b><a href="https://github.com/sponsors/dg" target="_blank" rel="noreferrer noopener">Please support Tracy via a donation 💙️</a></b></li>
|
||||
<li>Report generated at <?= date('Y/m/d H:i:s') ?></li>
|
||||
<?php foreach ($info as $item): ?><li><?= Helpers::escapeHtml($item) ?></li><?php endforeach ?>
|
||||
</ul>
|
||||
<div class="tracy-footer-logo"><a href="https://tracy.nette.org" rel="noreferrer"></a></div>
|
||||
</footer>
|
||||
</div>
|
||||
<meta itemprop=tracy-snapshot content=<?= Dumper::formatSnapshotAttribute($snapshot) ?>>
|
||||
</tracy-div>
|
55
libs/Nette/Tracy/BlueScreen/assets/page.phtml
Normal file
55
libs/Nette/Tracy/BlueScreen/assets/page.phtml
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $exception
|
||||
* @var string $title
|
||||
* @var ?string $nonce
|
||||
* @var string $css
|
||||
*/
|
||||
|
||||
$code = $exception->getCode() ? ' #' . $exception->getCode() : '';
|
||||
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
|
||||
$chain = Helpers::getExceptionChain($exception);
|
||||
?><!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></iframe></noembed></noframes></noscript></option></select></template></title></table></p></code>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="generator" content="Tracy by Nette Framework">
|
||||
|
||||
<title><?= Helpers::escapeHtml($title . ': ' . $exception->getMessage() . $code) ?></title>
|
||||
<!-- in <?= str_replace('--', '- ', Helpers::escapeHtml($exception->getFile() . ':' . $exception->getLine())) ?> -->
|
||||
<?php if (count($chain) > 1): ?>
|
||||
<!--<?php foreach (array_slice($chain, 1) as $ex) {
|
||||
echo str_replace('--', '- ', Helpers::escapeHtml("\n\tcaused by " . Helpers::getClass($ex) . ': ' . $ex->getMessage() . ($ex->getCode() ? ' #' . $ex->getCode() : '')));
|
||||
} ?> -->
|
||||
<?php endif ?>
|
||||
|
||||
<style class="tracy-debug">
|
||||
<?= str_replace('</', '<\/', $css) ?>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<?php require __DIR__ . '/content.phtml' ?>
|
||||
|
||||
<script<?= $nonceAttr ?>>
|
||||
'use strict';
|
||||
<?php
|
||||
array_map(function ($file) { echo '(function(){', str_replace(['<!--', '</s'], ['<\!--', '<\/s'], Helpers::minifyJs(file_get_contents($file))), '})();'; }, [
|
||||
__DIR__ . '/../../assets/toggle.js',
|
||||
__DIR__ . '/../../assets/table-sort.js',
|
||||
__DIR__ . '/../../assets/tabs.js',
|
||||
__DIR__ . '/../../Dumper/assets/dumper.js',
|
||||
__DIR__ . '/bluescreen.js',
|
||||
]);
|
||||
?>
|
||||
Tracy.BlueScreen.init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
36
libs/Nette/Tracy/BlueScreen/assets/section-cli.phtml
Normal file
36
libs/Nette/Tracy/BlueScreen/assets/section-cli.phtml
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var string $source
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
if (!Helpers::isCli()) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">CLI request</a></h2>
|
||||
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
<h3>Process ID <?= Helpers::escapeHtml(getmypid()) ?></h3>
|
||||
<?php if (count($tmp = explode('):', $source, 2)) === 2): ?>
|
||||
<pre>php<?= Helpers::escapeHtml($tmp[1]) ?></pre>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($_SERVER['argv'])): ?>
|
||||
<h3>Arguments</h3>
|
||||
<div class="tracy-pane">
|
||||
<table>
|
||||
<?php foreach ($_SERVER['argv'] as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
103
libs/Nette/Tracy/BlueScreen/assets/section-environment.phtml
Normal file
103
libs/Nette/Tracy/BlueScreen/assets/section-environment.phtml
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var callable $dump
|
||||
* @var bool $showEnvironment
|
||||
* @var array $obStatus
|
||||
* @var BlueScreen $this
|
||||
*/
|
||||
|
||||
if (!$showEnvironment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$constants = get_defined_constants(true)['user'] ?? [];
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Environment</a></h2>
|
||||
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
|
||||
<div class="tracy-tabs">
|
||||
<ul class="tracy-tab-bar">
|
||||
<li class="tracy-tab-label tracy-active"><a href="#">$_SERVER</a></li>
|
||||
<?php if ($_SESSION ?? null): ?>
|
||||
<li class="tracy-tab-label"><a href="#">$_SESSION</a></li>
|
||||
<?php endif ?>
|
||||
<?php if ($constants): ?>
|
||||
<li class="tracy-tab-label"><a href="#">Constants</a></li>
|
||||
<?php endif ?>
|
||||
<li class="tracy-tab-label"><a href="#">Configuration</a></li>
|
||||
<?php if ($obStatus): ?>
|
||||
<li class="tracy-tab-label"><a href="#">Output buffers</a></li>
|
||||
<?php endif ?>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="tracy-tab-panel tracy-pane tracy-active">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($_SERVER as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<?php if ($_SESSION ?? null): ?>
|
||||
<div class="tracy-tab-panel">
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($_SESSION as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $k === '__NF' ? '<i>Nette Session</i>' : $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<?php if (!empty($_SESSION['__NF']['DATA'])):?>
|
||||
<h3>Nette Session</h3>
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($_SESSION['__NF']['DATA'] as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
|
||||
<?php if ($constants): ?>
|
||||
<div class="tracy-tab-panel tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($constants as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<div class="tracy-tab-panel">
|
||||
<?php $this->renderPhpInfo() ?>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<?php if ($obStatus): ?>
|
||||
<div class="tracy-tab-panel tracy-pane">
|
||||
<?= Dumper::toHtml($obStatus, [Dumper::COLLAPSE_COUNT => 10]) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var \Throwable[] $exceptions
|
||||
* @var BlueScreen $this
|
||||
* @var array[] $actions
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
$ex = $ex->getPrevious();
|
||||
if (!$ex || in_array($ex, $exceptions, true)) {
|
||||
return;
|
||||
}
|
||||
$exceptions[] = $ex;
|
||||
?>
|
||||
|
||||
<section class="tracy-section" id="tracyCaused<?= count($exceptions) ?>">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = count($exceptions) > 1) ? ' tracy-collapsed' : '' ?>">Caused by</a></h2>
|
||||
|
||||
<div class="tracy-section-panel tracy-section--causedby<?= $collapsed ? ' tracy-collapsed' : '' ?>">
|
||||
<?php require __DIR__ . '/section-exception.phtml' ?>
|
||||
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
if (count((array) $ex) <= count((array) new \Exception)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Exception</a></h2>
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
<?= $dump($ex) ?>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
if (!$ex instanceof \ErrorException || empty($ex->context) || !is_array($ex->context)) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Variables</a></h2>
|
||||
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($ex->context as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
74
libs/Nette/Tracy/BlueScreen/assets/section-exception.phtml
Normal file
74
libs/Nette/Tracy/BlueScreen/assets/section-exception.phtml
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var \Throwable[] $exceptions
|
||||
* @var array[] $actions
|
||||
* @var callable $dump
|
||||
* @var BlueScreen $this
|
||||
* @var \Generator[] $generators
|
||||
* @var \Fiber[] $fibers
|
||||
*/
|
||||
|
||||
?>
|
||||
<?php require __DIR__ . '/section-header.phtml' ?>
|
||||
|
||||
<?php foreach ($this->renderPanels($ex) as $panel): ?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle"><?= Helpers::escapeHtml($panel->tab) ?></a></h2>
|
||||
|
||||
<div class="tracy-section-panel">
|
||||
<?= $panel->panel ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php if (!$exceptions && ($generators || $fibers)): ?>
|
||||
<section class="tracy-section tracy-section--stack">
|
||||
<div class="tracy-section-panel">
|
||||
<div class="tracy-tabs">
|
||||
<ul class="tracy-tab-bar">
|
||||
<li class="tracy-tab-label tracy-active"><a href="#">Main thread</a></li>
|
||||
|
||||
<?php foreach ($generators as $id => $generator): ?>
|
||||
<li class="tracy-tab-label"><a href="#">Generator #<?= $id ?></a></li>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php foreach ($fibers as $id => $fiber): ?>
|
||||
<li class="tracy-tab-label"><a href="#">Fiber #<?= $id ?></a></li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<div class="tracy-tab-panel tracy-active">
|
||||
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
|
||||
</div>
|
||||
|
||||
<?php foreach ($generators as $generator): ?>
|
||||
<div class="tracy-tab-panel">
|
||||
<?php require __DIR__ . '/section-stack-generator.phtml' ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php foreach ($fibers as $fiber): ?>
|
||||
<div class="tracy-tab-panel">
|
||||
<?php require __DIR__ . '/section-stack-fiber.phtml' ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<?php require __DIR__ . '/section-stack-exception.phtml' ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php require __DIR__ . '/section-exception-variables.phtml' ?>
|
||||
|
||||
<?php require __DIR__ . '/section-exception-exception.phtml' ?>
|
||||
|
||||
<?php require __DIR__ . '/section-exception-causedBy.phtml' ?>
|
35
libs/Nette/Tracy/BlueScreen/assets/section-header.phtml
Normal file
35
libs/Nette/Tracy/BlueScreen/assets/section-header.phtml
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var \Throwable[] $exceptions
|
||||
* @var array[] $actions
|
||||
* @var BlueScreen $this
|
||||
*/
|
||||
|
||||
$title = $ex instanceof \ErrorException
|
||||
? Helpers::errorTypeToString($ex->getSeverity())
|
||||
: Helpers::getClass($ex);
|
||||
$code = $ex->getCode() ? ' #' . $ex->getCode() : '';
|
||||
|
||||
?>
|
||||
<section class="tracy-section tracy-section--error">
|
||||
<?php if ($ex->getMessage()): ?><p><?= Helpers::escapeHtml($title . $code) ?></p><?php endif ?>
|
||||
|
||||
|
||||
<h1><span><?= $this->formatMessage($ex) ?: Helpers::escapeHtml($title . $code) ?></span>
|
||||
<?php foreach ($actions as $item): ?>
|
||||
<a href="<?= Helpers::escapeHtml($item['link']) ?>" class="tracy-action"<?= empty($item['external']) ? '' : ' target="_blank" rel="noreferrer noopener"'?>><?= Helpers::escapeHtml($item['label']) ?>►</a>
|
||||
<?php endforeach ?>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<?php if ($ex->getPrevious()): ?>
|
||||
<div class="tracy-caused">
|
||||
<a href="#tracyCaused<?= count($exceptions) + 1 ?>">Caused by <?= Helpers::escapeHtml(Helpers::getClass($ex->getPrevious())) ?></a>
|
||||
</div>
|
||||
<?php endif ?>
|
91
libs/Nette/Tracy/BlueScreen/assets/section-http.phtml
Normal file
91
libs/Nette/Tracy/BlueScreen/assets/section-http.phtml
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var string $source
|
||||
* @var string[] $httpHeaders
|
||||
* @var callable $dump
|
||||
* @var bool $headersSent
|
||||
* @var ?string $headersFile
|
||||
* @var ?int $headersLine
|
||||
*/
|
||||
|
||||
if (Helpers::isCli()) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">HTTP</a></h2>
|
||||
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
|
||||
<div class="tracy-tabs">
|
||||
<ul class="tracy-tab-bar">
|
||||
<li class="tracy-tab-label tracy-active"><a href="#">Request</a></li>
|
||||
<li class="tracy-tab-label"><a href="#">Response</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<div class="tracy-tab-panel tracy-active">
|
||||
<h3><?= Helpers::escapeHtml($_SERVER['REQUEST_METHOD'] ?? 'URL') ?> <a href="<?= Helpers::escapeHtml($source) ?>" target="_blank" rel="noreferrer noopener" style="font-weight: normal"><?= Helpers::escapeHtml($source) ?></a></h3>
|
||||
|
||||
<?php if ($httpHeaders): ?>
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($httpHeaders as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php foreach (['_GET', '_POST', '_COOKIE'] as $name): ?>
|
||||
<h3>$<?= Helpers::escapeHtml($name) ?></h3>
|
||||
<?php if (empty($GLOBALS[$name])):?>
|
||||
<p><i>empty</i></p>
|
||||
<?php else: ?>
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach ($GLOBALS[$name] as $k => $v): ?>
|
||||
<tr><th><?= Helpers::escapeHtml($k) ?></th><td><?= $dump($v, $k) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tracy-tab-panel">
|
||||
<h3>Code: <?= Helpers::escapeHtml(http_response_code()) ?></h3>
|
||||
<?php if (headers_list()): ?>
|
||||
<div class="tracy-pane">
|
||||
<table class="tracy-sortable">
|
||||
<?php foreach (headers_list() as $s): $s = explode(':', $s, 2); ?>
|
||||
<tr><th><?= Helpers::escapeHtml($s[0]) ?></th><td><?= $dump(trim($s[1]), $s[0]) ?></td></tr>
|
||||
<?php endforeach ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p><i>no headers</i></p>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php if ($headersSent && $headersFile && @is_file($headersFile)): ?>
|
||||
<p>Headers have been sent, output started at <?= Helpers::editorLink($headersFile, $headersLine) ?> <a href="#" data-tracy-ref="^p + div" class="tracy-toggle tracy-collapsed">source</a></p>
|
||||
<div class="tracy-collapsed"><?= BlueScreen::highlightFile($headersFile, $headersLine) ?></div>
|
||||
<?php elseif ($headersSent): ?>
|
||||
<p>Headers have been sent</p>
|
||||
<?php else: ?>
|
||||
<p>Headers were not sent at the time the exception was thrown</p>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var ?array $lastError
|
||||
*/
|
||||
|
||||
if (!$lastError) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle tracy-collapsed">Last muted error</a></h2>
|
||||
<div class="tracy-section-panel tracy-collapsed">
|
||||
|
||||
<h3><?= Helpers::errorTypeToString($lastError['type']) ?>: <?= Helpers::escapeHtml($lastError['message']) ?></h3>
|
||||
<p><i>Note: the last muted error may have nothing to do with the thrown exception.</i></p>
|
||||
|
||||
<?php if (isset($lastError['file']) && @is_file($lastError['file'])): // @ - may trigger error ?>
|
||||
<p><?= Helpers::editorLink($lastError['file'], $lastError['line']) ?></p>
|
||||
<div><?= BlueScreen::highlightFile($lastError['file'], $lastError['line']) ?></div>
|
||||
<?php else: ?>
|
||||
<p><i>inner-code</i><?php if (isset($lastError['line'])) echo ':', $lastError['line'] ?></p>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var callable $dump
|
||||
* @var int $expanded
|
||||
* @var array $stack
|
||||
*/
|
||||
|
||||
if (!$stack) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle">Call stack</a></h2>
|
||||
|
||||
<div class="tracy-section-panel">
|
||||
<div class="tracy-callstack">
|
||||
<?php foreach ($stack as $key => $row): ?>
|
||||
<?php $clickable = !empty($row['args']) || (isset($row['file']) && @is_file($row['file'])) // @ - may trigger error ?>
|
||||
|
||||
<div class="tracy-callstack-file">
|
||||
<?php if (isset($row['file']) && @is_file($row['file'])): // @ - may trigger error ?>
|
||||
<?= Helpers::editorLink($row['file'], $row['line']) ?>
|
||||
<?php else: ?>
|
||||
<i>inner-code</i><?php if (isset($row['line'])) echo ':', $row['line'] ?>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tracy-callstack-callee">
|
||||
<?php if ($clickable): ?>
|
||||
<a href="#" data-tracy-ref="^div + div" class="tracy-toggle<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>"><?php endif ?>
|
||||
<?php if (isset($row['class'])) echo Helpers::escapeHtml($row['class']), '::' ?><b><?= Helpers::escapeHtml($row['function']) ?></b> <?= empty($row['args']) ? '()' : '(...)' ?>
|
||||
<?php if ($clickable): ?></a><?php endif ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($clickable): ?>
|
||||
<div class="tracy-callstack-additional<?php if ($expanded !== $key) echo ' tracy-collapsed' ?>">
|
||||
<?php $sourceOriginal = isset($row['file']) && @is_file($row['file']) ? [$row['file'], $row['line']] : null // @ - may trigger error ?>
|
||||
<?php $sourceMapped = $sourceOriginal ? Debugger::mapSource(...$sourceOriginal) : null ?>
|
||||
<?php if ($sourceOriginal && $sourceMapped): ?>
|
||||
<div class="tracy-tabs">
|
||||
<ul class="tracy-tab-bar">
|
||||
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
|
||||
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
|
||||
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
|
||||
</div>
|
||||
|
||||
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
|
||||
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($sourceOriginal): ?>
|
||||
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
<?php if (!empty($row['args'])): ?>
|
||||
<table class="tracy-callstack-args">
|
||||
<?php
|
||||
try {
|
||||
$r = isset($row['class']) ? new \ReflectionMethod($row['class'], $row['function']) : new \ReflectionFunction($row['function']);
|
||||
$params = $r->getParameters();
|
||||
} catch (\Exception $e) {
|
||||
$params = [];
|
||||
}
|
||||
foreach ($row['args'] as $k => $v) {
|
||||
$argName = isset($params[$k]) && !$params[$k]->isVariadic() ? $params[$k]->name : $k;
|
||||
echo '<tr><th>', Helpers::escapeHtml((is_string($argName) ? '$' : '#') . $argName), '</th><td>';
|
||||
echo $dump($v, (string) $argName);
|
||||
echo "</td></tr>\n";
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Throwable $ex
|
||||
* @var callable $dump
|
||||
* @var BlueScreen $this
|
||||
*/
|
||||
|
||||
$stack = $ex->getTrace();
|
||||
$expanded = null;
|
||||
if (
|
||||
(!$ex instanceof \ErrorException
|
||||
|| in_array($ex->getSeverity(), [E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED], true))
|
||||
&& $this->isCollapsed($ex->getFile())
|
||||
) {
|
||||
foreach ($stack as $key => $row) {
|
||||
if (isset($row['file']) && !$this->isCollapsed($row['file'])) {
|
||||
$expanded = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)) {
|
||||
array_shift($stack);
|
||||
}
|
||||
if (($stack[0]['class'] ?? null) === Debugger::class && in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true)) {
|
||||
array_shift($stack);
|
||||
}
|
||||
$file = $ex->getFile();
|
||||
$line = $ex->getLine();
|
||||
|
||||
require __DIR__ . '/section-stack-sourceFile.phtml';
|
||||
require __DIR__ . '/section-stack-callStack.phtml';
|
16
libs/Nette/Tracy/BlueScreen/assets/section-stack-fiber.phtml
Normal file
16
libs/Nette/Tracy/BlueScreen/assets/section-stack-fiber.phtml
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Fiber $fiber
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
$ref = new \ReflectionFiber($fiber);
|
||||
$stack = $ref->getTrace();
|
||||
$expanded = 0;
|
||||
|
||||
require __DIR__ . '/section-stack-callStack.phtml';
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var \Generator $generator
|
||||
* @var callable $dump
|
||||
*/
|
||||
|
||||
$ref = new \ReflectionGenerator($generator);
|
||||
$stack = $ref->getTrace();
|
||||
$expanded = null;
|
||||
$execGenerator = $ref->getExecutingGenerator();
|
||||
$refExec = new \ReflectionGenerator($execGenerator);
|
||||
$file = $refExec->getExecutingFile();
|
||||
$line = $refExec->getExecutingLine();
|
||||
|
||||
require __DIR__ . '/section-stack-sourceFile.phtml';
|
||||
require __DIR__ . '/section-stack-callStack.phtml';
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var string $file
|
||||
* @var int $line
|
||||
* @var int $expanded
|
||||
*/
|
||||
|
||||
$sourceOriginal = $file && @is_file($file) ? [$file, $line] : null; // @ - may trigger error
|
||||
$sourceMapped = $sourceOriginal ? Debugger::mapSource($file, $line) : null;
|
||||
?>
|
||||
|
||||
<section class="tracy-section">
|
||||
<h2 class="tracy-section-label"><a href="#" data-tracy-ref="^+" class="tracy-toggle<?= ($collapsed = $expanded !== null) ? ' tracy-collapsed' : '' ?>">Source file</a></h2>
|
||||
|
||||
<div class="tracy-section-panel<?= $collapsed ? ' tracy-collapsed' : '' ?>">
|
||||
<?php if ($sourceOriginal && $sourceMapped): ?>
|
||||
<div class="tracy-tabs">
|
||||
<ul class="tracy-tab-bar">
|
||||
<li class="tracy-tab-label<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>"><a href="#">PHP</a></li>
|
||||
<li class="tracy-tab-label<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>"><a href="#"><?= Helpers::escapeHtml($sourceMapped['label']) ?></a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? '' : ' tracy-active' ?>">
|
||||
<p><b>File:</b> <?= Helpers::editorLink(...$sourceOriginal) ?></p>
|
||||
<?= BlueScreen::highlightFile(...$sourceOriginal) ?>
|
||||
</div>
|
||||
|
||||
<div class="tracy-tab-panel<?= $sourceMapped['active'] ? ' tracy-active' : '' ?>">
|
||||
<p><b>File:</b> <?= Helpers::editorLink($sourceMapped['file'], $sourceMapped['line']) ?></p>
|
||||
<?= BlueScreen::highlightFile($sourceMapped['file'], $sourceMapped['line'], 15, false) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p><b>File:</b> <?= Helpers::editorLink($file, $line) ?></p>
|
||||
<?php if ($sourceOriginal) echo BlueScreen::highlightFile(...$sourceOriginal) ?>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
</section>
|
658
libs/Nette/Tracy/Debugger/Debugger.php
Normal file
658
libs/Nette/Tracy/Debugger/Debugger.php
Normal file
@ -0,0 +1,658 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* Debugger: displays and logs errors.
|
||||
*/
|
||||
class Debugger
|
||||
{
|
||||
public const VERSION = '2.9.8';
|
||||
|
||||
/** server modes for Debugger::enable() */
|
||||
public const
|
||||
Development = false,
|
||||
Production = true,
|
||||
Detect = null;
|
||||
|
||||
public const
|
||||
DEVELOPMENT = self::Development,
|
||||
PRODUCTION = self::Production,
|
||||
DETECT = self::Detect;
|
||||
|
||||
public const CookieSecret = 'tracy-debug';
|
||||
public const COOKIE_SECRET = self::CookieSecret;
|
||||
|
||||
/** @var bool in production mode is suppressed any debugging output */
|
||||
public static $productionMode = self::Detect;
|
||||
|
||||
/** @var bool whether to display debug bar in development mode */
|
||||
public static $showBar = true;
|
||||
|
||||
/** @var bool whether to send data to FireLogger in development mode */
|
||||
public static $showFireLogger = true;
|
||||
|
||||
/** @var int size of reserved memory */
|
||||
public static $reservedMemorySize = 500000;
|
||||
|
||||
/** @var bool */
|
||||
private static $enabled = false;
|
||||
|
||||
/** @var string|null reserved memory; also prevents double rendering */
|
||||
private static $reserved;
|
||||
|
||||
/** @var int initial output buffer level */
|
||||
private static $obLevel;
|
||||
|
||||
/** @var ?array output buffer status @internal */
|
||||
public static $obStatus;
|
||||
|
||||
/********************* errors and exceptions reporting ****************d*g**/
|
||||
|
||||
/** @var bool|int determines whether any error will cause immediate death in development mode; if integer that it's matched against error severity */
|
||||
public static $strictMode = false;
|
||||
|
||||
/** @var bool|int disables the @ (shut-up) operator so that notices and warnings are no longer hidden; if integer than it's matched against error severity */
|
||||
public static $scream = false;
|
||||
|
||||
/** @var callable[] functions that are automatically called after fatal error */
|
||||
public static $onFatalError = [];
|
||||
|
||||
/********************* Debugger::dump() ****************d*g**/
|
||||
|
||||
/** @var int how many nested levels of array/object properties display by dump() */
|
||||
public static $maxDepth = 15;
|
||||
|
||||
/** @var int how long strings display by dump() */
|
||||
public static $maxLength = 150;
|
||||
|
||||
/** @var int how many items in array/object display by dump() */
|
||||
public static $maxItems = 100;
|
||||
|
||||
/** @var bool display location by dump()? */
|
||||
public static $showLocation;
|
||||
|
||||
/** @var string[] sensitive keys not displayed by dump() */
|
||||
public static $keysToHide = [];
|
||||
|
||||
/** @var string theme for dump() */
|
||||
public static $dumpTheme = 'light';
|
||||
|
||||
/** @deprecated */
|
||||
public static $maxLen;
|
||||
|
||||
/********************* logging ****************d*g**/
|
||||
|
||||
/** @var string|null name of the directory where errors should be logged */
|
||||
public static $logDirectory;
|
||||
|
||||
/** @var int log bluescreen in production mode for this error severity */
|
||||
public static $logSeverity = 0;
|
||||
|
||||
/** @var string|array email(s) to which send error notifications */
|
||||
public static $email;
|
||||
|
||||
/** for Debugger::log() and Debugger::fireLog() */
|
||||
public const
|
||||
DEBUG = ILogger::DEBUG,
|
||||
INFO = ILogger::INFO,
|
||||
WARNING = ILogger::WARNING,
|
||||
ERROR = ILogger::ERROR,
|
||||
EXCEPTION = ILogger::EXCEPTION,
|
||||
CRITICAL = ILogger::CRITICAL;
|
||||
|
||||
/********************* misc ****************d*g**/
|
||||
|
||||
/** @var float timestamp with microseconds of the start of the request */
|
||||
public static $time;
|
||||
|
||||
/** @var string URI pattern mask to open editor */
|
||||
public static $editor = 'editor://%action/?file=%file&line=%line&search=%search&replace=%replace';
|
||||
|
||||
/** @var array replacements in path */
|
||||
public static $editorMapping = [];
|
||||
|
||||
/** @var string command to open browser (use 'start ""' in Windows) */
|
||||
public static $browser;
|
||||
|
||||
/** @var string custom static error template */
|
||||
public static $errorTemplate;
|
||||
|
||||
/** @var string[] */
|
||||
public static $customCssFiles = [];
|
||||
|
||||
/** @var string[] */
|
||||
public static $customJsFiles = [];
|
||||
|
||||
/** @var callable[] */
|
||||
private static $sourceMappers = [];
|
||||
|
||||
/** @var array|null */
|
||||
private static $cpuUsage;
|
||||
|
||||
/********************* services ****************d*g**/
|
||||
|
||||
/** @var BlueScreen */
|
||||
private static $blueScreen;
|
||||
|
||||
/** @var Bar */
|
||||
private static $bar;
|
||||
|
||||
/** @var ILogger */
|
||||
private static $logger;
|
||||
|
||||
/** @var ILogger */
|
||||
private static $fireLogger;
|
||||
|
||||
/** @var array{DevelopmentStrategy, ProductionStrategy} */
|
||||
private static $strategy;
|
||||
|
||||
/** @var SessionStorage */
|
||||
private static $sessionStorage;
|
||||
|
||||
|
||||
/**
|
||||
* Static class - cannot be instantiated.
|
||||
*/
|
||||
final public function __construct()
|
||||
{
|
||||
throw new \LogicException;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enables displaying or logging errors and exceptions.
|
||||
* @param bool|string|string[] $mode use constant Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist.
|
||||
* @param string $logDirectory error log directory
|
||||
* @param string|array $email administrator email; enables email sending in production mode
|
||||
*/
|
||||
public static function enable($mode = null, ?string $logDirectory = null, $email = null): void
|
||||
{
|
||||
if ($mode !== null || self::$productionMode === null) {
|
||||
self::$productionMode = is_bool($mode)
|
||||
? $mode
|
||||
: !self::detectDebugMode($mode);
|
||||
}
|
||||
|
||||
self::$reserved = str_repeat('t', self::$reservedMemorySize);
|
||||
self::$time = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
|
||||
self::$obLevel = ob_get_level();
|
||||
self::$cpuUsage = !self::$productionMode && function_exists('getrusage') ? getrusage() : null;
|
||||
|
||||
// logging configuration
|
||||
if ($email !== null) {
|
||||
self::$email = $email;
|
||||
}
|
||||
|
||||
if ($logDirectory !== null) {
|
||||
self::$logDirectory = $logDirectory;
|
||||
}
|
||||
|
||||
if (self::$logDirectory) {
|
||||
if (!preg_match('#([a-z]+:)?[/\\\\]#Ai', self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException('Logging directory must be absolute path.'));
|
||||
exit(255);
|
||||
} elseif (!is_dir(self::$logDirectory)) {
|
||||
self::exceptionHandler(new \RuntimeException("Logging directory '" . self::$logDirectory . "' is not found."));
|
||||
exit(255);
|
||||
}
|
||||
}
|
||||
|
||||
// php configuration
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', '0'); // or 'stderr'
|
||||
ini_set('html_errors', '0');
|
||||
ini_set('log_errors', '0');
|
||||
ini_set('zend.exception_ignore_args', '0');
|
||||
}
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$strategy = self::getStrategy();
|
||||
$strategy->initialize();
|
||||
self::dispatch();
|
||||
|
||||
if (self::$enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_shutdown_function([self::class, 'shutdownHandler']);
|
||||
set_exception_handler(function (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
});
|
||||
set_error_handler([self::class, 'errorHandler']);
|
||||
|
||||
foreach ([
|
||||
'Bar/Bar',
|
||||
'Bar/DefaultBarPanel',
|
||||
'BlueScreen/BlueScreen',
|
||||
'Dumper/Describer',
|
||||
'Dumper/Dumper',
|
||||
'Dumper/Exposer',
|
||||
'Dumper/Renderer',
|
||||
'Dumper/Value',
|
||||
'Logger/FireLogger',
|
||||
'Logger/Logger',
|
||||
'Session/SessionStorage',
|
||||
'Session/FileSession',
|
||||
'Session/NativeSession',
|
||||
'Helpers',
|
||||
] as $path) {
|
||||
require_once dirname(__DIR__) . "/$path.php";
|
||||
}
|
||||
|
||||
self::$enabled = true;
|
||||
}
|
||||
|
||||
|
||||
public static function dispatch(): void
|
||||
{
|
||||
if (
|
||||
!Helpers::isCli()
|
||||
&& self::getStrategy()->sendAssets()
|
||||
) {
|
||||
self::$showBar = false;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders loading <script>
|
||||
*/
|
||||
public static function renderLoader(): void
|
||||
{
|
||||
self::getStrategy()->renderLoader();
|
||||
}
|
||||
|
||||
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::$enabled;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shutdown handler to catch fatal errors and execute of the planned activities.
|
||||
* @internal
|
||||
*/
|
||||
public static function shutdownHandler(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
|
||||
self::exceptionHandler(Helpers::fixStack(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'])));
|
||||
} elseif (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
self::$reserved = null;
|
||||
|
||||
if (self::$showBar && !Helpers::isCli()) {
|
||||
try {
|
||||
self::getStrategy()->renderBar();
|
||||
} catch (\Throwable $e) {
|
||||
self::exceptionHandler($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch uncaught exception.
|
||||
* @internal
|
||||
*/
|
||||
public static function exceptionHandler(\Throwable $exception): void
|
||||
{
|
||||
$firstTime = (bool) self::$reserved;
|
||||
self::$reserved = null;
|
||||
self::$obStatus = ob_get_status(true);
|
||||
|
||||
if (!headers_sent()) {
|
||||
http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE ') !== false ? 503 : 500);
|
||||
}
|
||||
|
||||
Helpers::improveException($exception);
|
||||
self::removeOutputBuffers(true);
|
||||
|
||||
self::getStrategy()->handleException($exception, $firstTime);
|
||||
|
||||
try {
|
||||
foreach ($firstTime ? self::$onFatalError : [] as $handler) {
|
||||
$handler($exception);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
self::log($e, self::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to catch warnings and notices.
|
||||
* @return bool|null false to call normal error handler, null otherwise
|
||||
* @throws ErrorException
|
||||
* @internal
|
||||
*/
|
||||
public static function errorHandler(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
?array $context = null
|
||||
): bool
|
||||
{
|
||||
$error = error_get_last();
|
||||
if (($error['type'] ?? null) === E_COMPILE_WARNING) {
|
||||
error_clear_last();
|
||||
self::errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
$context = (array) (object) $context; // workaround for PHP bug #80234
|
||||
}
|
||||
|
||||
if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
|
||||
if (Helpers::findTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), '*::__toString')) { // workaround for PHP < 7.4
|
||||
$previous = isset($context['e']) && $context['e'] instanceof \Throwable
|
||||
? $context['e']
|
||||
: null;
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line, $previous);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
self::exceptionHandler($e);
|
||||
exit(255);
|
||||
}
|
||||
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
throw $e;
|
||||
|
||||
} elseif (
|
||||
($severity & error_reporting())
|
||||
|| (is_int(self::$scream) ? $severity & self::$scream : self::$scream)
|
||||
) {
|
||||
self::getStrategy()->handleError($severity, $message, $file, $line, $context);
|
||||
}
|
||||
|
||||
return false; // calls normal error handler to fill-in error_get_last()
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function removeOutputBuffers(bool $errorOccurred): void
|
||||
{
|
||||
while (ob_get_level() > self::$obLevel) {
|
||||
$status = ob_get_status();
|
||||
if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$fnc = $status['chunk_size'] || !$errorOccurred
|
||||
? 'ob_end_flush'
|
||||
: 'ob_end_clean';
|
||||
if (!@$fnc()) { // @ may be not removable
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/********************* services ****************d*g**/
|
||||
|
||||
|
||||
public static function getBlueScreen(): BlueScreen
|
||||
{
|
||||
if (!self::$blueScreen) {
|
||||
self::$blueScreen = new BlueScreen;
|
||||
self::$blueScreen->info = [
|
||||
'PHP ' . PHP_VERSION,
|
||||
$_SERVER['SERVER_SOFTWARE'] ?? null,
|
||||
'Tracy ' . self::VERSION,
|
||||
];
|
||||
}
|
||||
|
||||
return self::$blueScreen;
|
||||
}
|
||||
|
||||
|
||||
public static function getBar(): Bar
|
||||
{
|
||||
if (!self::$bar) {
|
||||
self::$bar = new Bar;
|
||||
self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info');
|
||||
$info->cpuUsage = self::$cpuUsage;
|
||||
self::$bar->addPanel(new DefaultBarPanel('errors'), 'Tracy:errors'); // filled by errorHandler()
|
||||
}
|
||||
|
||||
return self::$bar;
|
||||
}
|
||||
|
||||
|
||||
public static function setLogger(ILogger $logger): void
|
||||
{
|
||||
self::$logger = $logger;
|
||||
}
|
||||
|
||||
|
||||
public static function getLogger(): ILogger
|
||||
{
|
||||
if (!self::$logger) {
|
||||
self::$logger = new Logger(self::$logDirectory, self::$email, self::getBlueScreen());
|
||||
self::$logger->directory = &self::$logDirectory; // back compatiblity
|
||||
self::$logger->email = &self::$email;
|
||||
}
|
||||
|
||||
return self::$logger;
|
||||
}
|
||||
|
||||
|
||||
public static function getFireLogger(): ILogger
|
||||
{
|
||||
if (!self::$fireLogger) {
|
||||
self::$fireLogger = new FireLogger;
|
||||
}
|
||||
|
||||
return self::$fireLogger;
|
||||
}
|
||||
|
||||
|
||||
/** @return ProductionStrategy|DevelopmentStrategy @internal */
|
||||
public static function getStrategy()
|
||||
{
|
||||
if (empty(self::$strategy[self::$productionMode])) {
|
||||
self::$strategy[self::$productionMode] = self::$productionMode
|
||||
? new ProductionStrategy
|
||||
: new DevelopmentStrategy(self::getBar(), self::getBlueScreen(), new DeferredContent(self::getSessionStorage()));
|
||||
}
|
||||
|
||||
return self::$strategy[self::$productionMode];
|
||||
}
|
||||
|
||||
|
||||
public static function setSessionStorage(SessionStorage $storage): void
|
||||
{
|
||||
if (self::$sessionStorage) {
|
||||
throw new \Exception('Storage is already set.');
|
||||
}
|
||||
|
||||
self::$sessionStorage = $storage;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function getSessionStorage(): SessionStorage
|
||||
{
|
||||
if (!self::$sessionStorage) {
|
||||
self::$sessionStorage = @is_dir($dir = session_save_path())
|
||||
|| @is_dir($dir = ini_get('upload_tmp_dir'))
|
||||
|| @is_dir($dir = sys_get_temp_dir())
|
||||
|| ($dir = self::$logDirectory)
|
||||
? new FileSession($dir)
|
||||
: new NativeSession;
|
||||
}
|
||||
|
||||
return self::$sessionStorage;
|
||||
}
|
||||
|
||||
|
||||
/********************* useful tools ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Dumps information about a variable in readable format.
|
||||
* @tracySkipLocation
|
||||
* @param mixed $var variable to dump
|
||||
* @param bool $return return output instead of printing it? (bypasses $productionMode)
|
||||
* @return mixed variable itself or dump
|
||||
*/
|
||||
public static function dump($var, bool $return = false)
|
||||
{
|
||||
if ($return) {
|
||||
$options = [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
];
|
||||
return Helpers::isCli()
|
||||
? Dumper::toText($var)
|
||||
: Helpers::capture(function () use ($var, $options) {
|
||||
Dumper::dump($var, $options);
|
||||
});
|
||||
|
||||
} elseif (!self::$productionMode) {
|
||||
$html = Helpers::isHtmlMode();
|
||||
echo $html ? '<tracy-div>' : '';
|
||||
Dumper::dump($var, [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
Dumper::LOCATION => self::$showLocation,
|
||||
Dumper::THEME => self::$dumpTheme,
|
||||
Dumper::KEYS_TO_HIDE => self::$keysToHide,
|
||||
]);
|
||||
echo $html ? '</tracy-div>' : '';
|
||||
}
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts/stops stopwatch.
|
||||
* @return float elapsed seconds
|
||||
*/
|
||||
public static function timer(?string $name = null): float
|
||||
{
|
||||
static $time = [];
|
||||
$now = microtime(true);
|
||||
$delta = isset($time[$name]) ? $now - $time[$name] : 0;
|
||||
$time[$name] = $now;
|
||||
return $delta;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps information about a variable in Tracy Debug Bar.
|
||||
* @tracySkipLocation
|
||||
* @param mixed $var
|
||||
* @return mixed variable itself
|
||||
*/
|
||||
public static function barDump($var, ?string $title = null, array $options = [])
|
||||
{
|
||||
if (!self::$productionMode) {
|
||||
static $panel;
|
||||
if (!$panel) {
|
||||
self::getBar()->addPanel($panel = new DefaultBarPanel('dumps'), 'Tracy:dumps');
|
||||
}
|
||||
|
||||
$panel->data[] = ['title' => $title, 'dump' => Dumper::toHtml($var, $options + [
|
||||
Dumper::DEPTH => self::$maxDepth,
|
||||
Dumper::ITEMS => self::$maxItems,
|
||||
Dumper::TRUNCATE => self::$maxLength,
|
||||
Dumper::LOCATION => self::$showLocation ?: Dumper::LOCATION_CLASS | Dumper::LOCATION_SOURCE,
|
||||
Dumper::LAZY => true,
|
||||
])];
|
||||
}
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logs message or exception.
|
||||
* @param mixed $message
|
||||
* @return mixed
|
||||
*/
|
||||
public static function log($message, string $level = ILogger::INFO)
|
||||
{
|
||||
return self::getLogger()->log($message, $level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends message to FireLogger console.
|
||||
* @param mixed $message
|
||||
*/
|
||||
public static function fireLog($message): bool
|
||||
{
|
||||
return !self::$productionMode && self::$showFireLogger
|
||||
? self::getFireLogger()->log($message)
|
||||
: false;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function addSourceMapper(callable $mapper): void
|
||||
{
|
||||
self::$sourceMappers[] = $mapper;
|
||||
}
|
||||
|
||||
|
||||
/** @return array{file: string, line: int, label: string, active: bool} */
|
||||
public static function mapSource(string $file, int $line): ?array
|
||||
{
|
||||
foreach (self::$sourceMappers as $mapper) {
|
||||
if ($res = $mapper($file, $line)) {
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects debug mode by IP address.
|
||||
* @param string|array $list IP addresses or computer names whitelist detection
|
||||
*/
|
||||
public static function detectDebugMode($list = null): bool
|
||||
{
|
||||
$addr = $_SERVER['REMOTE_ADDR'] ?? php_uname('n');
|
||||
$secret = isset($_COOKIE[self::CookieSecret]) && is_string($_COOKIE[self::CookieSecret])
|
||||
? $_COOKIE[self::CookieSecret]
|
||||
: null;
|
||||
$list = is_string($list)
|
||||
? preg_split('#[,\s]+#', $list)
|
||||
: (array) $list;
|
||||
if (!isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !isset($_SERVER['HTTP_FORWARDED'])) {
|
||||
$list[] = '127.0.0.1';
|
||||
$list[] = '::1';
|
||||
$list[] = '[::1]'; // workaround for PHP < 7.3.4
|
||||
}
|
||||
|
||||
return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
|
||||
}
|
||||
}
|
161
libs/Nette/Tracy/Debugger/DeferredContent.php
Normal file
161
libs/Nette/Tracy/Debugger/DeferredContent.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class DeferredContent
|
||||
{
|
||||
/** @var SessionStorage */
|
||||
private $sessionStorage;
|
||||
|
||||
/** @var string */
|
||||
private $requestId;
|
||||
|
||||
/** @var bool */
|
||||
private $useSession = false;
|
||||
|
||||
|
||||
public function __construct(SessionStorage $sessionStorage)
|
||||
{
|
||||
$this->sessionStorage = $sessionStorage;
|
||||
$this->requestId = $_SERVER['HTTP_X_TRACY_AJAX'] ?? Helpers::createId();
|
||||
}
|
||||
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->useSession && $this->sessionStorage->isAvailable();
|
||||
}
|
||||
|
||||
|
||||
public function getRequestId(): string
|
||||
{
|
||||
return $this->requestId;
|
||||
}
|
||||
|
||||
|
||||
public function &getItems(string $key): array
|
||||
{
|
||||
$items = &$this->sessionStorage->getData()[$key];
|
||||
$items = (array) $items;
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
public function addSetup(string $method, $argument): void
|
||||
{
|
||||
$argument = json_encode($argument, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
$item = &$this->getItems('setup')[$this->requestId];
|
||||
$item['code'] = ($item['code'] ?? '') . "$method($argument);\n";
|
||||
$item['time'] = time();
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
if (headers_sent($file, $line) || ob_get_length()) {
|
||||
throw new \LogicException(
|
||||
__METHOD__ . '() called after some output has been sent. '
|
||||
. ($file ? "Output started at $file:$line." : 'Try Tracy\OutputDebugger to find where output started.')
|
||||
);
|
||||
}
|
||||
|
||||
$asset = $_GET['_tracy_bar'] ?? null;
|
||||
if ($asset === 'js') {
|
||||
header('Content-Type: application/javascript; charset=UTF-8');
|
||||
header('Cache-Control: max-age=864000');
|
||||
header_remove('Pragma');
|
||||
header_remove('Set-Cookie');
|
||||
$str = $this->buildJsCss();
|
||||
header('Content-Length: ' . strlen($str));
|
||||
echo $str;
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->useSession = $this->sessionStorage->isAvailable();
|
||||
if (!$this->useSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->clean();
|
||||
|
||||
if (is_string($asset) && preg_match('#^content(-ajax)?\.(\w+)$#', $asset, $m)) {
|
||||
[, $ajax, $requestId] = $m;
|
||||
header('Content-Type: application/javascript; charset=UTF-8');
|
||||
header('Cache-Control: max-age=60');
|
||||
header_remove('Set-Cookie');
|
||||
$str = $ajax ? '' : $this->buildJsCss();
|
||||
$data = &$this->getItems('setup');
|
||||
$str .= $data[$requestId]['code'] ?? '';
|
||||
unset($data[$requestId]);
|
||||
header('Content-Length: ' . strlen($str));
|
||||
echo $str;
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Helpers::isAjax()) {
|
||||
header('X-Tracy-Ajax: 1'); // session must be already locked
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private function buildJsCss(): string
|
||||
{
|
||||
$css = array_map('file_get_contents', array_merge([
|
||||
__DIR__ . '/../assets/reset.css',
|
||||
__DIR__ . '/../Bar/assets/bar.css',
|
||||
__DIR__ . '/../assets/toggle.css',
|
||||
__DIR__ . '/../assets/table-sort.css',
|
||||
__DIR__ . '/../assets/tabs.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper-light.css',
|
||||
__DIR__ . '/../Dumper/assets/dumper-dark.css',
|
||||
__DIR__ . '/../BlueScreen/assets/bluescreen.css',
|
||||
], Debugger::$customCssFiles));
|
||||
|
||||
$js1 = array_map(function ($file) { return '(function() {' . file_get_contents($file) . '})();'; }, [
|
||||
__DIR__ . '/../Bar/assets/bar.js',
|
||||
__DIR__ . '/../assets/toggle.js',
|
||||
__DIR__ . '/../assets/table-sort.js',
|
||||
__DIR__ . '/../assets/tabs.js',
|
||||
__DIR__ . '/../Dumper/assets/dumper.js',
|
||||
__DIR__ . '/../BlueScreen/assets/bluescreen.js',
|
||||
]);
|
||||
$js2 = array_map('file_get_contents', Debugger::$customJsFiles);
|
||||
|
||||
$str = "'use strict';
|
||||
(function(){
|
||||
var el = document.createElement('style');
|
||||
el.setAttribute('nonce', document.currentScript.getAttribute('nonce') || document.currentScript.nonce);
|
||||
el.className='tracy-debug';
|
||||
el.textContent=" . json_encode(Helpers::minifyCss(implode('', $css))) . ";
|
||||
document.head.appendChild(el);})
|
||||
();\n" . implode('', $js1) . implode('', $js2);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
||||
public function clean(): void
|
||||
{
|
||||
foreach ($this->sessionStorage->getData() as &$items) {
|
||||
$items = array_slice((array) $items, -10, null, true);
|
||||
$items = array_filter($items, function ($item) {
|
||||
return isset($item['time']) && $item['time'] > time() - 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
141
libs/Nette/Tracy/Debugger/DevelopmentStrategy.php
Normal file
141
libs/Nette/Tracy/Debugger/DevelopmentStrategy.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class DevelopmentStrategy
|
||||
{
|
||||
/** @var Bar */
|
||||
private $bar;
|
||||
|
||||
/** @var BlueScreen */
|
||||
private $blueScreen;
|
||||
|
||||
/** @var DeferredContent */
|
||||
private $defer;
|
||||
|
||||
|
||||
public function __construct(Bar $bar, BlueScreen $blueScreen, DeferredContent $defer)
|
||||
{
|
||||
$this->bar = $bar;
|
||||
$this->blueScreen = $blueScreen;
|
||||
$this->defer = $defer;
|
||||
}
|
||||
|
||||
|
||||
public function initialize(): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function handleException(\Throwable $exception, bool $firstTime): void
|
||||
{
|
||||
if (Helpers::isAjax() && $this->defer->isAvailable()) {
|
||||
$this->blueScreen->renderToAjax($exception, $this->defer);
|
||||
|
||||
} elseif ($firstTime && Helpers::isHtmlMode()) {
|
||||
$this->blueScreen->render($exception);
|
||||
|
||||
} else {
|
||||
Debugger::fireLog($exception);
|
||||
$this->renderExceptionCli($exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function renderExceptionCli(\Throwable $exception): void
|
||||
{
|
||||
try {
|
||||
$logFile = Debugger::log($exception, Debugger::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
echo "$exception\nTracy is unable to log error: {$e->getMessage()}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($logFile && !headers_sent()) {
|
||||
header("X-Tracy-Error-Log: $logFile", false);
|
||||
}
|
||||
|
||||
if (Helpers::detectColors()) {
|
||||
echo "\n\n" . $this->blueScreen->highlightPhpCli($exception->getFile(), $exception->getLine()) . "\n";
|
||||
}
|
||||
|
||||
echo "$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : '');
|
||||
if ($logFile && Debugger::$browser) {
|
||||
exec(Debugger::$browser . ' ' . escapeshellarg(strtr($logFile, Debugger::$editorMapping)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleError(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
array $context = null
|
||||
): void
|
||||
{
|
||||
if (function_exists('ini_set')) {
|
||||
$oldDisplay = ini_set('display_errors', '1');
|
||||
}
|
||||
|
||||
if (
|
||||
(is_bool(Debugger::$strictMode) ? Debugger::$strictMode : (Debugger::$strictMode & $severity)) // $strictMode
|
||||
&& !isset($_GET['_tracy_skip_error'])
|
||||
) {
|
||||
$e = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$e->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
@$e->skippable = true;
|
||||
Debugger::exceptionHandler($e);
|
||||
exit(255);
|
||||
}
|
||||
|
||||
$message = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context);
|
||||
$count = &$this->bar->getPanel('Tracy:errors')->data["$file|$line|$message"];
|
||||
|
||||
if (!$count++) { // not repeated error
|
||||
Debugger::fireLog(new ErrorException($message, 0, $severity, $file, $line));
|
||||
if (!Helpers::isHtmlMode() && !Helpers::isAjax()) {
|
||||
echo "\n$message in $file on line $line\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', $oldDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
return $this->defer->sendAssets();
|
||||
}
|
||||
|
||||
|
||||
public function renderLoader(): void
|
||||
{
|
||||
$this->bar->renderLoader($this->defer);
|
||||
}
|
||||
|
||||
|
||||
public function renderBar(): void
|
||||
{
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('display_errors', '1');
|
||||
}
|
||||
|
||||
$this->bar->render($this->defer);
|
||||
}
|
||||
}
|
95
libs/Nette/Tracy/Debugger/ProductionStrategy.php
Normal file
95
libs/Nette/Tracy/Debugger/ProductionStrategy.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ProductionStrategy
|
||||
{
|
||||
public function initialize(): void
|
||||
{
|
||||
if (!function_exists('ini_set') && (ini_get('display_errors') && ini_get('display_errors') !== 'stderr')) {
|
||||
Debugger::exceptionHandler(new \RuntimeException("Unable to set 'display_errors' because function ini_set() is disabled."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleException(\Throwable $exception, bool $firstTime): void
|
||||
{
|
||||
try {
|
||||
Debugger::log($exception, Debugger::EXCEPTION);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
if (!$firstTime) {
|
||||
// nothing
|
||||
|
||||
} elseif (Helpers::isHtmlMode()) {
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
}
|
||||
|
||||
(function ($logged) use ($exception) {
|
||||
require Debugger::$errorTemplate ?: __DIR__ . '/assets/error.500.phtml';
|
||||
})(empty($e));
|
||||
|
||||
} elseif (Helpers::isCli()) {
|
||||
// @ triggers E_NOTICE when strerr is closed since PHP 7.4
|
||||
@fwrite(STDERR, "ERROR: {$exception->getMessage()}\n"
|
||||
. (isset($e)
|
||||
? 'Unable to log error. You may try enable debug mode to inspect the problem.'
|
||||
: 'Check log to see more info.')
|
||||
. "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function handleError(
|
||||
int $severity,
|
||||
string $message,
|
||||
string $file,
|
||||
int $line,
|
||||
array $context = null
|
||||
): void
|
||||
{
|
||||
if ($severity & Debugger::$logSeverity) {
|
||||
$err = new ErrorException($message, 0, $severity, $file, $line);
|
||||
@$err->context = $context; // dynamic properties are deprecated since PHP 8.2
|
||||
Helpers::improveException($err);
|
||||
} else {
|
||||
$err = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message, (array) $context) . " in $file:$line";
|
||||
}
|
||||
|
||||
try {
|
||||
Debugger::log($err, Debugger::ERROR);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function sendAssets(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function renderLoader(): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function renderBar(): void
|
||||
{
|
||||
}
|
||||
}
|
43
libs/Nette/Tracy/Debugger/assets/error.500.phtml
Normal file
43
libs/Nette/Tracy/Debugger/assets/error.500.phtml
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Default error page.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
/**
|
||||
* @var bool $logged
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></audio></button></canvas></datalist></details></dialog></iframe></listing></meter></noembed></noframes></noscript></optgroup></option></progress></rp></select></table></template></title></video>
|
||||
<meta charset="utf-8">
|
||||
<meta name=robots content=noindex>
|
||||
<meta name=generator content="Tracy">
|
||||
<title>Server Error</title>
|
||||
|
||||
<style>
|
||||
#tracy-error { all: initial; position: absolute; top: 0; left: 0; right: 0; height: 70vh; min-height: 400px; display: flex; align-items: center; justify-content: center; z-index: 1000 }
|
||||
#tracy-error div { all: initial; max-width: 550px; background: white; color: #333; display: block }
|
||||
#tracy-error h1 { all: initial; font: bold 50px/1.1 sans-serif; display: block; margin: 40px }
|
||||
#tracy-error p { all: initial; font: 20px/1.4 sans-serif; margin: 40px; display: block }
|
||||
#tracy-error small { color: gray }
|
||||
#tracy-error small span { color: silver }
|
||||
</style>
|
||||
|
||||
<div id=tracy-error>
|
||||
<div>
|
||||
<h1>Server Error</h1>
|
||||
|
||||
<p>We're sorry! The server encountered an internal error and
|
||||
was unable to complete your request. Please try again later.</p>
|
||||
|
||||
<p><small>error 500 <span> | <?php echo date('j. n. Y H:i') ?></span><?php if (!$logged): ?><br>Tracy is unable to log error.<?php endif ?></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.body.insertBefore(document.getElementById('tracy-error'), document.body.firstChild);
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user