One Hat Cyber Team
Your IP:
216.73.216.30
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
self
/
root
/
proc
/
thread-self
/
cwd
/
Edit File:
library.tar
helpers.php 0000644 00000002446 15107542733 0006734 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ use Mockery\Matcher\AndAnyOtherArgs; use Mockery\Matcher\AnyArgs; if (!function_exists("mock")) { function mock(...$args) { return Mockery::mock(...$args); } } if (!function_exists("spy")) { function spy(...$args) { return Mockery::spy(...$args); } } if (!function_exists("namedMock")) { function namedMock(...$args) { return Mockery::namedMock(...$args); } } if (!function_exists("anyArgs")) { function anyArgs() { return new AnyArgs(); } } if (!function_exists("andAnyOtherArgs")) { function andAnyOtherArgs() { return new AndAnyOtherArgs(); } } if (!function_exists("andAnyOthers")) { function andAnyOthers() { return new AndAnyOtherArgs(); } } // //$currentDirectory = dirname(__FILE__,2); //$libraryDirectory = $currentDirectory . '/library'; //if (! file_exists($libraryDirectory)) //{ // symlink( // $currentDirectory . '/library', // $libraryDirectory // ); //} Mockery.php 0000644 00000060227 15107542733 0006704 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ use Mockery\ClosureWrapper; use Mockery\ExpectationInterface; use Mockery\Generator\CachingGenerator; use Mockery\Generator\Generator; use Mockery\Generator\MockConfigurationBuilder; use Mockery\Generator\MockNameBuilder; use Mockery\Generator\StringManipulationGenerator; use Mockery\Loader\EvalLoader; use Mockery\Loader\Loader; use Mockery\Matcher\MatcherAbstract; use Mockery\Reflector; class Mockery { const BLOCKS = 'Mockery_Forward_Blocks'; /** * Global container to hold all mocks for the current unit test running. * * @var \Mockery\Container|null */ protected static $_container = null; /** * Global configuration handler containing configuration options. * * @var \Mockery\Configuration */ protected static $_config = null; /** * @var \Mockery\Generator\Generator */ protected static $_generator; /** * @var \Mockery\Loader\Loader */ protected static $_loader; /** * @var array */ private static $_filesToCleanUp = []; /** * Defines the global helper functions * * @return void */ public static function globalHelpers() { require_once __DIR__ . '/helpers.php'; } /** * @return array * * @deprecated since 1.3.2 and will be removed in 2.0. */ public static function builtInTypes() { return array( 'array', 'bool', 'callable', 'float', 'int', 'iterable', 'object', 'self', 'string', 'void', ); } /** * @param string $type * @return bool * * @deprecated since 1.3.2 and will be removed in 2.0. */ public static function isBuiltInType($type) { return in_array($type, \Mockery::builtInTypes()); } /** * Static shortcut to \Mockery\Container::mock(). * * @param mixed ...$args * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public static function mock(...$args) { return call_user_func_array(array(self::getContainer(), 'mock'), $args); } /** * Static and semantic shortcut for getting a mock from the container * and applying the spy's expected behavior into it. * * @param mixed ...$args * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public static function spy(...$args) { if (count($args) && $args[0] instanceof \Closure) { $args[0] = new ClosureWrapper($args[0]); } return call_user_func_array(array(self::getContainer(), 'mock'), $args)->shouldIgnoreMissing(); } /** * Static and Semantic shortcut to \Mockery\Container::mock(). * * @param mixed ...$args * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public static function instanceMock(...$args) { return call_user_func_array(array(self::getContainer(), 'mock'), $args); } /** * Static shortcut to \Mockery\Container::mock(), first argument names the mock. * * @param mixed ...$args * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public static function namedMock(...$args) { $name = array_shift($args); $builder = new MockConfigurationBuilder(); $builder->setName($name); array_unshift($args, $builder); return call_user_func_array(array(self::getContainer(), 'mock'), $args); } /** * Static shortcut to \Mockery\Container::self(). * * @throws LogicException * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public static function self() { if (is_null(self::$_container)) { throw new \LogicException('You have not declared any mocks yet'); } return self::$_container->self(); } /** * Static shortcut to closing up and verifying all mocks in the global * container, and resetting the container static variable to null. * * @return void */ public static function close() { foreach (self::$_filesToCleanUp as $fileName) { @unlink($fileName); } self::$_filesToCleanUp = []; if (is_null(self::$_container)) { return; } $container = self::$_container; self::$_container = null; $container->mockery_teardown(); $container->mockery_close(); } /** * Static fetching of a mock associated with a name or explicit class poser. * * @param string $name * * @return \Mockery\Mock */ public static function fetchMock($name) { return self::getContainer()->fetchMock($name); } /** * Lazy loader and getter for * the container property. * * @return Mockery\Container */ public static function getContainer() { if (is_null(self::$_container)) { self::$_container = new Mockery\Container(self::getGenerator(), self::getLoader()); } return self::$_container; } /** * Setter for the $_generator static property. * * @param \Mockery\Generator\Generator $generator */ public static function setGenerator(Generator $generator) { self::$_generator = $generator; } /** * Lazy loader method and getter for * the generator property. * * @return Generator */ public static function getGenerator() { if (is_null(self::$_generator)) { self::$_generator = self::getDefaultGenerator(); } return self::$_generator; } /** * Creates and returns a default generator * used inside this class. * * @return CachingGenerator */ public static function getDefaultGenerator() { return new CachingGenerator(StringManipulationGenerator::withDefaultPasses()); } /** * Setter for the $_loader static property. * * @param Loader $loader */ public static function setLoader(Loader $loader) { self::$_loader = $loader; } /** * Lazy loader method and getter for * the $_loader property. * * @return Loader */ public static function getLoader() { if (is_null(self::$_loader)) { self::$_loader = self::getDefaultLoader(); } return self::$_loader; } /** * Gets an EvalLoader to be used as default. * * @return EvalLoader */ public static function getDefaultLoader() { return new EvalLoader(); } /** * Set the container. * * @param \Mockery\Container $container * * @return \Mockery\Container */ public static function setContainer(Mockery\Container $container) { return self::$_container = $container; } /** * Reset the container to null. * * @return void */ public static function resetContainer() { self::$_container = null; } /** * Return instance of ANY matcher. * * @return \Mockery\Matcher\Any */ public static function any() { return new \Mockery\Matcher\Any(); } /** * Return instance of AndAnyOtherArgs matcher. * * An alternative name to `andAnyOtherArgs` so * the API stays closer to `any` as well. * * @return \Mockery\Matcher\AndAnyOtherArgs */ public static function andAnyOthers() { return new \Mockery\Matcher\AndAnyOtherArgs(); } /** * Return instance of AndAnyOtherArgs matcher. * * @return \Mockery\Matcher\AndAnyOtherArgs */ public static function andAnyOtherArgs() { return new \Mockery\Matcher\AndAnyOtherArgs(); } /** * Return instance of TYPE matcher. * * @param mixed $expected * * @return \Mockery\Matcher\Type */ public static function type($expected) { return new \Mockery\Matcher\Type($expected); } /** * Return instance of DUCKTYPE matcher. * * @param array ...$args * * @return \Mockery\Matcher\Ducktype */ public static function ducktype(...$args) { return new \Mockery\Matcher\Ducktype($args); } /** * Return instance of SUBSET matcher. * * @param array $part * @param bool $strict - (Optional) True for strict comparison, false for loose * * @return \Mockery\Matcher\Subset */ public static function subset(array $part, $strict = true) { return new \Mockery\Matcher\Subset($part, $strict); } /** * Return instance of CONTAINS matcher. * * @param mixed $args * * @return \Mockery\Matcher\Contains */ public static function contains(...$args) { return new \Mockery\Matcher\Contains($args); } /** * Return instance of HASKEY matcher. * * @param mixed $key * * @return \Mockery\Matcher\HasKey */ public static function hasKey($key) { return new \Mockery\Matcher\HasKey($key); } /** * Return instance of HASVALUE matcher. * * @param mixed $val * * @return \Mockery\Matcher\HasValue */ public static function hasValue($val) { return new \Mockery\Matcher\HasValue($val); } /** * Return instance of CLOSURE matcher. * * @param $reference * * @return \Mockery\Matcher\Closure */ public static function capture(&$reference) { $closure = function ($argument) use (&$reference) { $reference = $argument; return true; }; return new \Mockery\Matcher\Closure($closure); } /** * Return instance of CLOSURE matcher. * * @param mixed $closure * * @return \Mockery\Matcher\Closure */ public static function on($closure) { return new \Mockery\Matcher\Closure($closure); } /** * Return instance of MUSTBE matcher. * * @param mixed $expected * * @return \Mockery\Matcher\MustBe */ public static function mustBe($expected) { return new \Mockery\Matcher\MustBe($expected); } /** * Return instance of NOT matcher. * * @param mixed $expected * * @return \Mockery\Matcher\Not */ public static function not($expected) { return new \Mockery\Matcher\Not($expected); } /** * Return instance of ANYOF matcher. * * @param array ...$args * * @return \Mockery\Matcher\AnyOf */ public static function anyOf(...$args) { return new \Mockery\Matcher\AnyOf($args); } /** * Return instance of NOTANYOF matcher. * * @param array ...$args * * @return \Mockery\Matcher\NotAnyOf */ public static function notAnyOf(...$args) { return new \Mockery\Matcher\NotAnyOf($args); } /** * Return instance of PATTERN matcher. * * @param mixed $expected * * @return \Mockery\Matcher\Pattern */ public static function pattern($expected) { return new \Mockery\Matcher\Pattern($expected); } /** * Lazy loader and Getter for the global * configuration container. * * @return \Mockery\Configuration */ public static function getConfiguration() { if (is_null(self::$_config)) { self::$_config = new \Mockery\Configuration(); } return self::$_config; } /** * Utility method to format method name and arguments into a string. * * @param string $method * @param array $arguments * * @return string */ public static function formatArgs($method, array $arguments = null) { if (is_null($arguments)) { return $method . '()'; } $formattedArguments = array(); foreach ($arguments as $argument) { $formattedArguments[] = self::formatArgument($argument); } return $method . '(' . implode(', ', $formattedArguments) . ')'; } /** * Gets the string representation * of any passed argument. * * @param mixed $argument * @param int $depth * * @return mixed */ private static function formatArgument($argument, $depth = 0) { if ($argument instanceof MatcherAbstract) { return (string) $argument; } if (is_object($argument)) { return 'object(' . get_class($argument) . ')'; } if (is_int($argument) || is_float($argument)) { return $argument; } if (is_array($argument)) { if ($depth === 1) { $argument = '[...]'; } else { $sample = array(); foreach ($argument as $key => $value) { $key = is_int($key) ? $key : "'$key'"; $value = self::formatArgument($value, $depth + 1); $sample[] = "$key => $value"; } $argument = "[" . implode(", ", $sample) . "]"; } return ((strlen($argument) > 1000) ? substr($argument, 0, 1000) . '...]' : $argument); } if (is_bool($argument)) { return $argument ? 'true' : 'false'; } if (is_resource($argument)) { return 'resource(...)'; } if (is_null($argument)) { return 'NULL'; } return "'" . (string) $argument . "'"; } /** * Utility function to format objects to printable arrays. * * @param array $objects * * @return string */ public static function formatObjects(array $objects = null) { static $formatting; if ($formatting) { return '[Recursion]'; } if (is_null($objects)) { return ''; } $objects = array_filter($objects, 'is_object'); if (empty($objects)) { return ''; } $formatting = true; $parts = array(); foreach ($objects as $object) { $parts[get_class($object)] = self::objectToArray($object); } $formatting = false; return 'Objects: ( ' . var_export($parts, true) . ')'; } /** * Utility function to turn public properties and public get* and is* method values into an array. * * @param object $object * @param int $nesting * * @return array */ private static function objectToArray($object, $nesting = 3) { if ($nesting == 0) { return array('...'); } $defaultFormatter = function ($object, $nesting) { return array('properties' => self::extractInstancePublicProperties($object, $nesting)); }; $class = get_class($object); $formatter = self::getConfiguration()->getObjectFormatter($class, $defaultFormatter); $array = array( 'class' => $class, 'identity' => '#' . md5(spl_object_hash($object)) ); $array = array_merge($array, $formatter($object, $nesting)); return $array; } /** * Returns all public instance properties. * * @param mixed $object * @param int $nesting * * @return array */ private static function extractInstancePublicProperties($object, $nesting) { $reflection = new \ReflectionClass(get_class($object)); $properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC); $cleanedProperties = array(); foreach ($properties as $publicProperty) { if (!$publicProperty->isStatic()) { $name = $publicProperty->getName(); try { $cleanedProperties[$name] = self::cleanupNesting($object->$name, $nesting); } catch (\Exception $exception) { $cleanedProperties[$name] = $exception->getMessage(); } } } return $cleanedProperties; } /** * Utility method used for recursively generating * an object or array representation. * * @param mixed $argument * @param int $nesting * * @return mixed */ private static function cleanupNesting($argument, $nesting) { if (is_object($argument)) { $object = self::objectToArray($argument, $nesting - 1); $object['class'] = get_class($argument); return $object; } if (is_array($argument)) { return self::cleanupArray($argument, $nesting - 1); } return $argument; } /** * Utility method for recursively * gerating a representation * of the given array. * * @param array $argument * @param int $nesting * * @return mixed */ private static function cleanupArray($argument, $nesting = 3) { if ($nesting == 0) { return '...'; } foreach ($argument as $key => $value) { if (is_array($value)) { $argument[$key] = self::cleanupArray($value, $nesting - 1); } elseif (is_object($value)) { $argument[$key] = self::objectToArray($value, $nesting - 1); } } return $argument; } /** * Utility function to parse shouldReceive() arguments and generate * expectations from such as needed. * * @param Mockery\LegacyMockInterface $mock * @param array ...$args * @param callable $add * @return \Mockery\CompositeExpectation */ public static function parseShouldReturnArgs(\Mockery\LegacyMockInterface $mock, $args, $add) { $composite = new \Mockery\CompositeExpectation(); foreach ($args as $arg) { if (is_array($arg)) { foreach ($arg as $k => $v) { $expectation = self::buildDemeterChain($mock, $k, $add)->andReturn($v); $composite->add($expectation); } } elseif (is_string($arg)) { $expectation = self::buildDemeterChain($mock, $arg, $add); $composite->add($expectation); } } return $composite; } /** * Sets up expectations on the members of the CompositeExpectation and * builds up any demeter chain that was passed to shouldReceive. * * @param \Mockery\LegacyMockInterface $mock * @param string $arg * @param callable $add * @throws Mockery\Exception * @return \Mockery\ExpectationInterface */ protected static function buildDemeterChain(\Mockery\LegacyMockInterface $mock, $arg, $add) { /** @var Mockery\Container $container */ $container = $mock->mockery_getContainer(); $methodNames = explode('->', $arg); reset($methodNames); if (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && !$mock->mockery_isAnonymous() && !in_array(current($methodNames), $mock->mockery_getMockableMethods()) ) { throw new \Mockery\Exception( 'Mockery\'s configuration currently forbids mocking the method ' . current($methodNames) . ' as it does not exist on the class or object ' . 'being mocked' ); } /** @var ExpectationInterface|null $expectations */ $expectations = null; /** @var Callable $nextExp */ $nextExp = function ($method) use ($add) { return $add($method); }; $parent = get_class($mock); while (true) { $method = array_shift($methodNames); $expectations = $mock->mockery_getExpectationsFor($method); if (is_null($expectations) || self::noMoreElementsInChain($methodNames)) { $expectations = $nextExp($method); if (self::noMoreElementsInChain($methodNames)) { break; } $mock = self::getNewDemeterMock($container, $parent, $method, $expectations); } else { $demeterMockKey = $container->getKeyOfDemeterMockFor($method, $parent); if ($demeterMockKey) { $mock = self::getExistingDemeterMock($container, $demeterMockKey); } } $parent .= '->' . $method; $nextExp = function ($n) use ($mock) { return $mock->shouldReceive($n); }; } return $expectations; } /** * Gets a new demeter configured * mock from the container. * * @param \Mockery\Container $container * @param string $parent * @param string $method * @param Mockery\ExpectationInterface $exp * * @return \Mockery\Mock */ private static function getNewDemeterMock( Mockery\Container $container, $parent, $method, Mockery\ExpectationInterface $exp ) { $newMockName = 'demeter_' . md5($parent) . '_' . $method; $parRef = null; $parRefMethod = null; $parRefMethodRetType = null; $parentMock = $exp->getMock(); if ($parentMock !== null) { $parRef = new ReflectionObject($parentMock); } if ($parRef !== null && $parRef->hasMethod($method)) { $parRefMethod = $parRef->getMethod($method); $parRefMethodRetType = Reflector::getReturnType($parRefMethod, true); if ($parRefMethodRetType !== null && $parRefMethodRetType !== 'mixed') { $nameBuilder = new MockNameBuilder(); $nameBuilder->addPart('\\' . $newMockName); $mock = self::namedMock($nameBuilder->build(), $parRefMethodRetType); $exp->andReturn($mock); return $mock; } } $mock = $container->mock($newMockName); $exp->andReturn($mock); return $mock; } /** * Gets an specific demeter mock from * the ones kept by the container. * * @param \Mockery\Container $container * @param string $demeterMockKey * * @return mixed */ private static function getExistingDemeterMock( Mockery\Container $container, $demeterMockKey ) { $mocks = $container->getMocks(); $mock = $mocks[$demeterMockKey]; return $mock; } /** * Checks if the passed array representing a demeter * chain with the method names is empty. * * @param array $methodNames * * @return bool */ private static function noMoreElementsInChain(array $methodNames) { return empty($methodNames); } public static function declareClass($fqn) { return static::declareType($fqn, "class"); } public static function declareInterface($fqn) { return static::declareType($fqn, "interface"); } private static function declareType($fqn, $type) { $targetCode = "<?php "; $shortName = $fqn; if (strpos($fqn, "\\")) { $parts = explode("\\", $fqn); $shortName = trim(array_pop($parts)); $namespace = implode("\\", $parts); $targetCode.= "namespace $namespace;\n"; } $targetCode.= "$type $shortName {} "; /* * We could eval here, but it doesn't play well with the way * PHPUnit tries to backup global state and the require definition * loader */ $tmpfname = tempnam(sys_get_temp_dir(), "Mockery"); file_put_contents($tmpfname, $targetCode); require $tmpfname; \Mockery::registerFileForCleanUp($tmpfname); } /** * Register a file to be deleted on tearDown. * * @param string $fileName */ public static function registerFileForCleanUp($fileName) { self::$_filesToCleanUp[] = $fileName; } } Mockery/HigherOrderMessage.php 0000644 00000001664 15107542733 0012413 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; /** * @method \Mockery\Expectation withArgs(\Closure|array $args) */ class HigherOrderMessage { private $mock; private $method; public function __construct(MockInterface $mock, $method) { $this->mock = $mock; $this->method = $method; } /** * @return \Mockery\Expectation */ public function __call($method, $args) { if ($this->method === 'shouldNotHaveReceived') { return $this->mock->{$this->method}($method, $args); } $expectation = $this->mock->{$this->method}($method); return $expectation->withArgs($args); } } Mockery/ExpectationInterface.php 0000644 00000001276 15107542733 0013007 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; interface ExpectationInterface { /** * @return int */ public function getOrderNumber(); /** * @return LegacyMockInterface|MockInterface */ public function getMock(); /** * @param mixed $args * @return self */ public function andReturn(...$args); /** * @return self */ public function andReturns(); } Mockery/LegacyMockInterface.php 0000644 00000013161 15107542733 0012536 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; interface LegacyMockInterface { /** * Alternative setup method to constructor * * @param \Mockery\Container $container * @param object $partialObject * @return void */ public function mockery_init(\Mockery\Container $container = null, $partialObject = null); /** * Set expected method calls * * @param string|array ...$methodNames one or many methods that are expected to be called in this mock * * @return \Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function shouldReceive(...$methodNames); /** * Shortcut method for setting an expectation that a method should not be called. * * @param string|array ...$methodNames one or many methods that are expected not to be called in this mock * @return \Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function shouldNotReceive(...$methodNames); /** * Allows additional methods to be mocked that do not explicitly exist on mocked class * @param String $method name of the method to be mocked */ public function shouldAllowMockingMethod($method); /** * Set mock to ignore unexpected methods and return Undefined class * @param mixed $returnValue the default return value for calls to missing functions on this mock * @return static */ public function shouldIgnoreMissing($returnValue = null); /** * @return static */ public function shouldAllowMockingProtectedMethods(); /** * Set mock to defer unexpected methods to its parent if possible * * @deprecated since 1.4.0. Please use makePartial() instead. * * @return static */ public function shouldDeferMissing(); /** * Set mock to defer unexpected methods to its parent if possible * * @return static */ public function makePartial(); /** * @param null|string $method * @param null|array|Closure $args * @return mixed */ public function shouldHaveReceived($method, $args = null); /** * @return mixed */ public function shouldHaveBeenCalled(); /** * @param null|string $method * @param null|array|Closure $args * @return mixed */ public function shouldNotHaveReceived($method, $args = null); /** * @param array $args (optional) * @return mixed */ public function shouldNotHaveBeenCalled(array $args = null); /** * In the event shouldReceive() accepting an array of methods/returns * this method will switch them from normal expectations to default * expectations * * @return self */ public function byDefault(); /** * Iterate across all expectation directors and validate each * * @throws \Mockery\CountValidator\Exception * @return void */ public function mockery_verify(); /** * Tear down tasks for this mock * * @return void */ public function mockery_teardown(); /** * Fetch the next available allocation order number * * @return int */ public function mockery_allocateOrder(); /** * Set ordering for a group * * @param mixed $group * @param int $order */ public function mockery_setGroup($group, $order); /** * Fetch array of ordered groups * * @return array */ public function mockery_getGroups(); /** * Set current ordered number * * @param int $order */ public function mockery_setCurrentOrder($order); /** * Get current ordered number * * @return int */ public function mockery_getCurrentOrder(); /** * Validate the current mock's ordering * * @param string $method * @param int $order * @throws \Mockery\Exception * @return void */ public function mockery_validateOrder($method, $order); /** * Gets the count of expectations for this mock * * @return int */ public function mockery_getExpectationCount(); /** * Return the expectations director for the given method * * @var string $method * @return \Mockery\ExpectationDirector|null */ public function mockery_setExpectationsFor($method, \Mockery\ExpectationDirector $director); /** * Return the expectations director for the given method * * @var string $method * @return \Mockery\ExpectationDirector|null */ public function mockery_getExpectationsFor($method); /** * Find an expectation matching the given method and arguments * * @var string $method * @var array $args * @return \Mockery\Expectation|null */ public function mockery_findExpectation($method, array $args); /** * Return the container for this mock * * @return \Mockery\Container */ public function mockery_getContainer(); /** * Return the name for this mock * * @return string */ public function mockery_getName(); /** * @return array */ public function mockery_getMockableProperties(); /** * @return string[] */ public function mockery_getMockableMethods(); /** * @return bool */ public function mockery_isAnonymous(); } Mockery/ReceivedMethodCalls.php 0000644 00000001655 15107542733 0012552 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class ReceivedMethodCalls { private $methodCalls = array(); public function push(MethodCall $methodCall) { $this->methodCalls[] = $methodCall; } public function verify(Expectation $expectation) { foreach ($this->methodCalls as $methodCall) { if ($methodCall->getMethod() !== $expectation->getName()) { continue; } if (!$expectation->matchArgs($methodCall->getArgs())) { continue; } $expectation->verifyCall($methodCall->getArgs()); } $expectation->verify(); } } Mockery/Adapter/Phpunit/TestListenerTrait.php 0000644 00000005017 15107542733 0015340 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Adapter\Phpunit; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Util\Blacklist; use PHPUnit\Runner\BaseTestRunner; class TestListenerTrait { /** * endTest is called after each test and checks if \Mockery::close() has * been called, and will let the test fail if it hasn't. * * @param Test $test * @param float $time */ public function endTest(Test $test, $time) { if (!$test instanceof TestCase) { // We need the getTestResultObject and getStatus methods which are // not part of the interface. return; } if ($test->getStatus() !== BaseTestRunner::STATUS_PASSED) { // If the test didn't pass there is no guarantee that // verifyMockObjects and assertPostConditions have been called. // And even if it did, the point here is to prevent false // negatives, not to make failing tests fail for more reasons. return; } try { // The self() call is used as a sentinel. Anything that throws if // the container is closed already will do. \Mockery::self(); } catch (\LogicException $_) { return; } $e = new ExpectationFailedException( \sprintf( "Mockery's expectations have not been verified. Make sure that \Mockery::close() is called at the end of the test. Consider using %s\MockeryPHPUnitIntegration or extending %s\MockeryTestCase.", __NAMESPACE__, __NAMESPACE__ ) ); /** @var \PHPUnit\Framework\TestResult $result */ $result = $test->getTestResultObject(); if ($result !== null) { $result->addFailure($test, $e, $time); } } public function startTestSuite() { if (method_exists(Blacklist::class, 'addDirectory')) { (new BlackList())->getBlacklistedDirectories(); Blacklist::addDirectory(\dirname((new \ReflectionClass(\Mockery::class))->getFileName())); } else { Blacklist::$blacklistedClassNames[\Mockery::class] = 1; } } } Mockery/Adapter/Phpunit/TestListener.php 0000644 00000001676 15107542733 0014343 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Adapter\Phpunit; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestListener as PHPUnitTestListener; class TestListener implements PHPUnitTestListener { use TestListenerDefaultImplementation; private $trait; public function __construct() { $this->trait = new TestListenerTrait(); } public function endTest(Test $test, float $time): void { $this->trait->endTest($test, $time); } public function startTestSuite(TestSuite $suite): void { $this->trait->startTestSuite(); } } Mockery/Adapter/Phpunit/MockeryTestCase.php 0000644 00000001131 15107542733 0014745 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Adapter\Phpunit; use PHPUnit\Framework\TestCase; abstract class MockeryTestCase extends TestCase { use MockeryPHPUnitIntegration; use MockeryTestCaseSetUp; protected function mockeryTestSetUp() { } protected function mockeryTestTearDown() { } } Mockery/Adapter/Phpunit/MockeryPHPUnitIntegrationAssertPostConditions.php 0000644 00000001023 15107542733 0023007 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ declare(strict_types=1); namespace Mockery\Adapter\Phpunit; trait MockeryPHPUnitIntegrationAssertPostConditions { protected function assertPostConditions(): void { $this->mockeryAssertPostConditions(); } } Mockery/Adapter/Phpunit/MockeryPHPUnitIntegration.php 0000644 00000003673 15107542733 0016742 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Adapter\Phpunit; use Mockery; /** * Integrates Mockery into PHPUnit. Ensures Mockery expectations are verified * for each test and are included by the assertion counter. */ trait MockeryPHPUnitIntegration { use MockeryPHPUnitIntegrationAssertPostConditions; protected $mockeryOpen; /** * Performs assertions shared by all tests of a test case. This method is * called before execution of a test ends and before the tearDown method. */ protected function mockeryAssertPostConditions() { $this->addMockeryExpectationsToAssertionCount(); $this->checkMockeryExceptions(); $this->closeMockery(); parent::assertPostConditions(); } protected function addMockeryExpectationsToAssertionCount() { $this->addToAssertionCount(Mockery::getContainer()->mockery_getExpectationCount()); } protected function checkMockeryExceptions() { if (!method_exists($this, "markAsRisky")) { return; } foreach (Mockery::getContainer()->mockery_thrownExceptions() as $e) { if (!$e->dismissed()) { $this->markAsRisky(); } } } protected function closeMockery() { Mockery::close(); $this->mockeryOpen = false; } /** * @before */ protected function startMockery() { $this->mockeryOpen = true; } /** * @after */ protected function purgeMockeryContainer() { if ($this->mockeryOpen) { // post conditions wasn't called, so test probably failed Mockery::close(); } } } Mockery/Adapter/Phpunit/MockeryTestCaseSetUp.php 0000644 00000001160 15107542733 0015730 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ declare(strict_types=1); namespace Mockery\Adapter\Phpunit; trait MockeryTestCaseSetUp { protected function setUp(): void { parent::setUp(); $this->mockeryTestSetUp(); } protected function tearDown(): void { $this->mockeryTestTearDown(); parent::tearDown(); } } Mockery/Reflector.php 0000644 00000020327 15107542733 0010626 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use ReflectionType; /** * @internal */ class Reflector { private const TRAVERSABLE_ARRAY = ['\Traversable', 'array']; private const ITERABLE = ['iterable']; /** * Determine if the parameter is typed as an array. * * @param \ReflectionParameter $param * * @return bool */ public static function isArray(\ReflectionParameter $param) { $type = $param->getType(); return $type instanceof \ReflectionNamedType && $type->getName(); } /** * Compute the string representation for the paramater type. * * @param \ReflectionParameter $param * @param bool $withoutNullable * * @return string|null */ public static function getTypeHint(\ReflectionParameter $param, $withoutNullable = false) { if (!$param->hasType()) { return null; } $type = $param->getType(); $declaringClass = $param->getDeclaringClass(); $typeHint = self::getTypeFromReflectionType($type, $declaringClass); return (!$withoutNullable && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint; } /** * Compute the string representation for the return type. * * @param \ReflectionParameter $param * @param bool $withoutNullable * * @return string|null */ public static function getReturnType(\ReflectionMethod $method, $withoutNullable = false) { $type = $method->getReturnType(); if (!$type instanceof ReflectionType && method_exists($method, 'getTentativeReturnType')) { $type = $method->getTentativeReturnType(); } if (!$type instanceof ReflectionType) { return null; } $typeHint = self::getTypeFromReflectionType($type, $method->getDeclaringClass()); return (!$withoutNullable && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint; } /** * Compute the string representation for the simplest return type. * * @param \ReflectionParameter $param * * @return string|null */ public static function getSimplestReturnType(\ReflectionMethod $method) { $type = $method->getReturnType(); if (!$type instanceof ReflectionType && method_exists($method, 'getTentativeReturnType')) { $type = $method->getTentativeReturnType(); } if (!$type instanceof ReflectionType || $type->allowsNull()) { return null; } $typeInformation = self::getTypeInformation($type, $method->getDeclaringClass()); // return the first primitive type hint foreach ($typeInformation as $info) { if ($info['isPrimitive']) { return $info['typeHint']; } } // if no primitive type, return the first type foreach ($typeInformation as $info) { return $info['typeHint']; } return null; } /** * Get the string representation of the given type. * * @param \ReflectionType $type * @param \ReflectionClass $declaringClass * * @return list<array{typeHint: string, isPrimitive: bool}> */ private static function getTypeInformation(\ReflectionType $type, \ReflectionClass $declaringClass) { // PHP 8 union types and PHP 8.1 intersection types can be recursively processed if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { $types = []; foreach ($type->getTypes() as $innterType) { foreach (self::getTypeInformation($innterType, $declaringClass) as $info) { if ($info['typeHint'] === 'null' && $info['isPrimitive']) { continue; } $types[] = $info; } } return $types; } // $type must be an instance of \ReflectionNamedType $typeHint = $type->getName(); // builtins can be returned as is if ($type->isBuiltin()) { return [ [ 'typeHint' => $typeHint, 'isPrimitive' => in_array($typeHint, ['array', 'bool', 'int', 'float', 'null', 'object', 'string']), ], ]; } // 'static' can be returned as is if ($typeHint === 'static') { return [ [ 'typeHint' => $typeHint, 'isPrimitive' => false, ], ]; } // 'self' needs to be resolved to the name of the declaring class if ($typeHint === 'self') { $typeHint = $declaringClass->getName(); } // 'parent' needs to be resolved to the name of the parent class if ($typeHint === 'parent') { $typeHint = $declaringClass->getParentClass()->getName(); } // class names need prefixing with a slash return [ [ 'typeHint' => sprintf('\\%s', $typeHint), 'isPrimitive' => false, ], ]; } /** * Format the given type as a nullable type. * * @param string $typeHint * * @return string */ private static function formatNullableType($typeHint) { if ($typeHint === 'mixed') { return $typeHint; } if (strpos($typeHint, 'null') !== false) { return $typeHint; } if (\PHP_VERSION_ID < 80000) { return sprintf('?%s', $typeHint); } return sprintf('%s|null', $typeHint); } private static function getTypeFromReflectionType(\ReflectionType $type, \ReflectionClass $declaringClass): string { if ($type instanceof \ReflectionNamedType) { $typeHint = $type->getName(); if ($type->isBuiltin()) { return $typeHint; } if ($typeHint === 'static') { return $typeHint; } // 'self' needs to be resolved to the name of the declaring class if ($typeHint === 'self'){ $typeHint = $declaringClass->getName(); } // 'parent' needs to be resolved to the name of the parent class if ($typeHint === 'parent'){ $typeHint = $declaringClass->getParentClass()->getName(); } // class names need prefixing with a slash return sprintf('\\%s', $typeHint); } if ($type instanceof \ReflectionIntersectionType) { $types = array_map( static function (\ReflectionType $type) use ($declaringClass): string { return self::getTypeFromReflectionType($type, $declaringClass); }, $type->getTypes() ); return implode( '&', $types, ); } if ($type instanceof \ReflectionUnionType) { $types = array_map( static function (\ReflectionType $type) use ($declaringClass): string { return self::getTypeFromReflectionType($type, $declaringClass); }, $type->getTypes() ); $intersect = array_intersect(self::TRAVERSABLE_ARRAY, $types); if (self::TRAVERSABLE_ARRAY === $intersect) { $types = array_merge(self::ITERABLE, array_diff($types, self::TRAVERSABLE_ARRAY)); } return implode( '|', array_map( static function (string $type): string { return strpos($type, '&') === false ? $type : sprintf('(%s)', $type); }, $types ) ); } throw new \InvalidArgumentException('Unknown ReflectionType: ' . get_debug_type($type)); } } Mockery/ExpectsHigherOrderMessage.php 0000644 00000001277 15107542733 0013747 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class ExpectsHigherOrderMessage extends HigherOrderMessage { public function __construct(MockInterface $mock) { parent::__construct($mock, "shouldReceive"); } /** * @return \Mockery\Expectation */ public function __call($method, $args) { $expectation = parent::__call($method, $args); return $expectation->once(); } } Mockery/Loader/RequireLoader.php 0000644 00000002625 15107542733 0012653 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Loader; use Mockery\Generator\MockDefinition; use Mockery\Loader\Loader; class RequireLoader implements Loader { /** * @var string */ protected $path; /** * @var string */ protected $lastPath = ''; public function __construct($path = null) { $this->path = realpath($path) ?: sys_get_temp_dir(); register_shutdown_function([$this, '__destruct']); } public function __destruct() { $files = array_diff( glob($this->path . DIRECTORY_SEPARATOR . 'Mockery_*.php')?:[], [$this->lastPath] ); foreach ($files as $file) { @unlink($file); } } public function load(MockDefinition $definition) { if (class_exists($definition->getClassName(), false)) { return; } $this->lastPath = sprintf('%s%s%s.php', $this->path, DIRECTORY_SEPARATOR, uniqid('Mockery_')); file_put_contents($this->lastPath, $definition->getCode()); if (file_exists($this->lastPath)){ require $this->lastPath; } } } Mockery/Loader/Loader.php 0000644 00000000673 15107542733 0011317 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Loader; use Mockery\Generator\MockDefinition; interface Loader { public function load(MockDefinition $definition); } Mockery/Loader/EvalLoader.php 0000644 00000001177 15107542733 0012127 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Loader; use Mockery\Generator\MockDefinition; use Mockery\Loader\Loader; class EvalLoader implements Loader { public function load(MockDefinition $definition) { if (class_exists($definition->getClassName(), false)) { return; } eval("?>" . $definition->getCode()); } } Mockery/ClosureWrapper.php 0000644 00000001171 15107542733 0011652 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Mockery\Matcher\Closure; /** * @internal */ class ClosureWrapper { private $closure; public function __construct(\Closure $closure) { $this->closure = $closure; } public function __invoke() { return call_user_func_array($this->closure, func_get_args()); } } Mockery/MockInterface.php 0000644 00000001556 15107542733 0011416 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Mockery\LegacyMockInterface; interface MockInterface extends LegacyMockInterface { /** * @param mixed $something String method name or map of method => return * @return self|\Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function allows($something = []); /** * @param mixed $something String method name (optional) * @return \Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\ExpectsHigherOrderMessage */ public function expects($something = null); } Mockery/CompositeExpectation.php 0000644 00000007044 15107542733 0013050 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class CompositeExpectation implements ExpectationInterface { /** * Stores an array of all expectations for this composite * * @var array */ protected $_expectations = array(); /** * Add an expectation to the composite * * @param \Mockery\Expectation|\Mockery\CompositeExpectation $expectation * @return void */ public function add($expectation) { $this->_expectations[] = $expectation; } /** * @param mixed ...$args */ public function andReturn(...$args) { return $this->__call(__FUNCTION__, $args); } /** * Set a return value, or sequential queue of return values * * @param mixed ...$args * @return self */ public function andReturns(...$args) { return call_user_func_array([$this, 'andReturn'], $args); } /** * Intercept any expectation calls and direct against all expectations * * @param string $method * @param array $args * @return self */ public function __call($method, array $args) { foreach ($this->_expectations as $expectation) { call_user_func_array(array($expectation, $method), $args); } return $this; } /** * Return order number of the first expectation * * @return int */ public function getOrderNumber() { reset($this->_expectations); $first = current($this->_expectations); return $first->getOrderNumber(); } /** * Return the parent mock of the first expectation * * @return \Mockery\MockInterface|\Mockery\LegacyMockInterface */ public function getMock() { reset($this->_expectations); $first = current($this->_expectations); return $first->getMock(); } /** * Mockery API alias to getMock * * @return \Mockery\LegacyMockInterface|\Mockery\MockInterface */ public function mock() { return $this->getMock(); } /** * Starts a new expectation addition on the first mock which is the primary * target outside of a demeter chain * * @param mixed ...$args * @return \Mockery\Expectation */ public function shouldReceive(...$args) { reset($this->_expectations); $first = current($this->_expectations); return call_user_func_array(array($first->getMock(), 'shouldReceive'), $args); } /** * Starts a new expectation addition on the first mock which is the primary * target outside of a demeter chain * * @param mixed ...$args * @return \Mockery\Expectation */ public function shouldNotReceive(...$args) { reset($this->_expectations); $first = current($this->_expectations); return call_user_func_array(array($first->getMock(), 'shouldNotReceive'), $args); } /** * Return the string summary of this composite expectation * * @return string */ public function __toString() { $return = '['; $parts = array(); foreach ($this->_expectations as $exp) { $parts[] = (string) $exp; } $return .= implode(', ', $parts) . ']'; return $return; } } Mockery/Expectation.php 0000644 00000055534 15107542733 0011174 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Closure; use Mockery\Matcher\NoArgs; use Mockery\Matcher\AnyArgs; use Mockery\Matcher\AndAnyOtherArgs; use Mockery\Matcher\ArgumentListMatcher; use Mockery\Matcher\MultiArgumentClosure; class Expectation implements ExpectationInterface { /** * Mock object to which this expectation belongs * * @var \Mockery\LegacyMockInterface */ protected $_mock = null; /** * Method name * * @var string */ protected $_name = null; /** * Exception message * * @var string|null */ protected $_because = null; /** * Arguments expected by this expectation * * @var array */ protected $_expectedArgs = array(); /** * Count validator store * * @var array */ protected $_countValidators = array(); /** * The count validator class to use * * @var string */ protected $_countValidatorClass = 'Mockery\CountValidator\Exact'; /** * Actual count of calls to this expectation * * @var int */ protected $_actualCount = 0; /** * Value to return from this expectation * * @var mixed */ protected $_returnValue = null; /** * Array of return values as a queue for multiple return sequence * * @var array */ protected $_returnQueue = array(); /** * Array of closures executed with given arguments to generate a result * to be returned * * @var array */ protected $_closureQueue = array(); /** * Array of values to be set when this expectation matches * * @var array */ protected $_setQueue = array(); /** * Integer representing the call order of this expectation * * @var int */ protected $_orderNumber = null; /** * Integer representing the call order of this expectation on a global basis * * @var int */ protected $_globalOrderNumber = null; /** * Flag indicating that an exception is expected to be throw (not returned) * * @var bool */ protected $_throw = false; /** * Flag indicating whether the order of calling is determined locally or * globally * * @var bool */ protected $_globally = false; /** * Flag indicating if the return value should be obtained from the original * class method instead of returning predefined values from the return queue * * @var bool */ protected $_passthru = false; /** * Constructor * * @param \Mockery\LegacyMockInterface $mock * @param string $name */ public function __construct(\Mockery\LegacyMockInterface $mock, $name) { $this->_mock = $mock; $this->_name = $name; $this->withAnyArgs(); } /** * Return a string with the method name and arguments formatted * * @param string $name Name of the expected method * @param array $args List of arguments to the method * @return string */ public function __toString() { return \Mockery::formatArgs($this->_name, $this->_expectedArgs); } /** * Verify the current call, i.e. that the given arguments match those * of this expectation * * @param array $args * @return mixed */ public function verifyCall(array $args) { $this->validateOrder(); $this->_actualCount++; if (true === $this->_passthru) { return $this->_mock->mockery_callSubjectMethod($this->_name, $args); } $return = $this->_getReturnValue($args); $this->throwAsNecessary($return); $this->_setValues(); return $return; } /** * Throws an exception if the expectation has been configured to do so * * @throws \Throwable * @return void */ private function throwAsNecessary($return) { if (!$this->_throw) { return; } if ($return instanceof \Throwable) { throw $return; } return; } /** * Sets public properties with queued values to the mock object * * @param array $args * @return mixed */ protected function _setValues() { $mockClass = get_class($this->_mock); $container = $this->_mock->mockery_getContainer(); /** @var Mock[] $mocks */ $mocks = $container->getMocks(); foreach ($this->_setQueue as $name => &$values) { if (count($values) > 0) { $value = array_shift($values); $this->_mock->{$name} = $value; foreach ($mocks as $mock) { if (is_a($mock, $mockClass) && $mock->mockery_isInstance()) { $mock->{$name} = $value; } } } } } /** * Fetch the return value for the matching args * * @param array $args * @return mixed */ protected function _getReturnValue(array $args) { if (count($this->_closureQueue) > 1) { return call_user_func_array(array_shift($this->_closureQueue), $args); } elseif (count($this->_closureQueue) > 0) { return call_user_func_array(current($this->_closureQueue), $args); } elseif (count($this->_returnQueue) > 1) { return array_shift($this->_returnQueue); } elseif (count($this->_returnQueue) > 0) { return current($this->_returnQueue); } return $this->_mock->mockery_returnValueForMethod($this->_name); } /** * Checks if this expectation is eligible for additional calls * * @return bool */ public function isEligible() { foreach ($this->_countValidators as $validator) { if (!$validator->isEligible($this->_actualCount)) { return false; } } return true; } /** * Check if there is a constraint on call count * * @return bool */ public function isCallCountConstrained() { return (count($this->_countValidators) > 0); } /** * Verify call order * * @return void */ public function validateOrder() { if ($this->_orderNumber) { $this->_mock->mockery_validateOrder((string) $this, $this->_orderNumber, $this->_mock); } if ($this->_globalOrderNumber) { $this->_mock->mockery_getContainer() ->mockery_validateOrder((string) $this, $this->_globalOrderNumber, $this->_mock); } } /** * Verify this expectation * * @return void */ public function verify() { foreach ($this->_countValidators as $validator) { $validator->validate($this->_actualCount); } } /** * Check if the registered expectation is an ArgumentListMatcher * @return bool */ private function isArgumentListMatcher() { return (count($this->_expectedArgs) === 1 && ($this->_expectedArgs[0] instanceof ArgumentListMatcher)); } private function isAndAnyOtherArgumentsMatcher($expectedArg) { return $expectedArg instanceof AndAnyOtherArgs; } /** * Check if passed arguments match an argument expectation * * @param array $args * @return bool */ public function matchArgs(array $args) { if ($this->isArgumentListMatcher()) { return $this->_matchArg($this->_expectedArgs[0], $args); } $argCount = count($args); if ($argCount !== count((array) $this->_expectedArgs)) { $lastExpectedArgument = end($this->_expectedArgs); reset($this->_expectedArgs); if ($this->isAndAnyOtherArgumentsMatcher($lastExpectedArgument)) { $args = array_slice($args, 0, array_search($lastExpectedArgument, $this->_expectedArgs, true)); return $this->_matchArgs($args); } return false; } return $this->_matchArgs($args); } /** * Check if the passed arguments match the expectations, one by one. * * @param array $args * @return bool */ protected function _matchArgs($args) { $argCount = count($args); for ($i=0; $i<$argCount; $i++) { $param =& $args[$i]; if (!$this->_matchArg($this->_expectedArgs[$i], $param)) { return false; } } return true; } /** * Check if passed argument matches an argument expectation * * @param mixed $expected * @param mixed $actual * @return bool */ protected function _matchArg($expected, &$actual) { if ($expected === $actual) { return true; } if (!is_object($expected) && !is_object($actual) && $expected == $actual) { return true; } if (is_string($expected) && is_object($actual)) { $result = $actual instanceof $expected; if ($result) { return true; } } if (is_object($expected)) { $matcher = \Mockery::getConfiguration()->getDefaultMatcher(get_class($expected)); if ($matcher !== null) { $expected = new $matcher($expected); } } if ($expected instanceof \Mockery\Matcher\MatcherAbstract) { return $expected->match($actual); } if ($expected instanceof \Hamcrest\Matcher || $expected instanceof \Hamcrest_Matcher) { @trigger_error('Hamcrest package has been deprecated and will be removed in 2.0', E_USER_DEPRECATED); return $expected->matches($actual); } return false; } /** * Expected argument setter for the expectation * * @param mixed ...$args * * @return self */ public function with(...$args) { return $this->withArgs($args); } /** * Expected arguments for the expectation passed as an array * * @param array $arguments * @return self */ private function withArgsInArray(array $arguments) { if (empty($arguments)) { return $this->withNoArgs(); } $this->_expectedArgs = $arguments; return $this; } /** * Expected arguments have to be matched by the given closure. * * @param Closure $closure * @return self */ private function withArgsMatchedByClosure(Closure $closure) { $this->_expectedArgs = [new MultiArgumentClosure($closure)]; return $this; } /** * Expected arguments for the expectation passed as an array or a closure that matches each passed argument on * each function call. * * @param array|Closure $argsOrClosure * @return self */ public function withArgs($argsOrClosure) { if (is_array($argsOrClosure)) { $this->withArgsInArray($argsOrClosure); } elseif ($argsOrClosure instanceof Closure) { $this->withArgsMatchedByClosure($argsOrClosure); } else { throw new \InvalidArgumentException(sprintf('Call to %s with an invalid argument (%s), only array and ' . 'closure are allowed', __METHOD__, $argsOrClosure)); } return $this; } /** * Set with() as no arguments expected * * @return self */ public function withNoArgs() { $this->_expectedArgs = [new NoArgs()]; return $this; } /** * Set expectation that any arguments are acceptable * * @return self */ public function withAnyArgs() { $this->_expectedArgs = [new AnyArgs()]; return $this; } /** * Expected arguments should partially match the real arguments * * @param mixed ...$expectedArgs * @return self */ public function withSomeOfArgs(...$expectedArgs) { return $this->withArgs(function (...$args) use ($expectedArgs) { foreach ($expectedArgs as $expectedArg) { if (!in_array($expectedArg, $args, true)) { return false; } } return true; }); } /** * Set a return value, or sequential queue of return values * * @param mixed ...$args * @return self */ public function andReturn(...$args) { $this->_returnQueue = $args; return $this; } /** * Set a return value, or sequential queue of return values * * @param mixed ...$args * @return self */ public function andReturns(...$args) { return call_user_func_array([$this, 'andReturn'], $args); } /** * Return this mock, like a fluent interface * * @return self */ public function andReturnSelf() { return $this->andReturn($this->_mock); } /** * Set a sequential queue of return values with an array * * @param array $values * @return self */ public function andReturnValues(array $values) { call_user_func_array(array($this, 'andReturn'), $values); return $this; } /** * Set a closure or sequence of closures with which to generate return * values. The arguments passed to the expected method are passed to the * closures as parameters. * * @param callable ...$args * @return self */ public function andReturnUsing(...$args) { $this->_closureQueue = $args; return $this; } /** * Sets up a closure to return the nth argument from the expected method call * * @param int $index * @return self */ public function andReturnArg($index) { if (!is_int($index) || $index < 0) { throw new \InvalidArgumentException("Invalid argument index supplied. Index must be a non-negative integer."); } $closure = function (...$args) use ($index) { if (array_key_exists($index, $args)) { return $args[$index]; } throw new \OutOfBoundsException("Cannot return an argument value. No argument exists for the index $index"); }; $this->_closureQueue = [$closure]; return $this; } /** * Return a self-returning black hole object. * * @return self */ public function andReturnUndefined() { $this->andReturn(new \Mockery\Undefined()); return $this; } /** * Return null. This is merely a language construct for Mock describing. * * @return self */ public function andReturnNull() { return $this->andReturn(null); } public function andReturnFalse() { return $this->andReturn(false); } public function andReturnTrue() { return $this->andReturn(true); } /** * Set Exception class and arguments to that class to be thrown * * @param string|\Exception $exception * @param string $message * @param int $code * @param \Exception $previous * @return self */ public function andThrow($exception, $message = '', $code = 0, \Exception $previous = null) { $this->_throw = true; if (is_object($exception)) { $this->andReturn($exception); } else { $this->andReturn(new $exception($message, $code, $previous)); } return $this; } public function andThrows($exception, $message = '', $code = 0, \Exception $previous = null) { return $this->andThrow($exception, $message, $code, $previous); } /** * Set Exception classes to be thrown * * @param array $exceptions * @return self */ public function andThrowExceptions(array $exceptions) { $this->_throw = true; foreach ($exceptions as $exception) { if (!is_object($exception)) { throw new Exception('You must pass an array of exception objects to andThrowExceptions'); } } return $this->andReturnValues($exceptions); } /** * Register values to be set to a public property each time this expectation occurs * * @param string $name * @param array ...$values * @return self */ public function andSet($name, ...$values) { $this->_setQueue[$name] = $values; return $this; } /** * Sets up a closure that will yield each of the provided args * * @param mixed ...$args * @return self */ public function andYield(...$args) { $this->_closureQueue = [ static function () use ($args) { foreach ($args as $arg) { yield $arg; } }, ]; return $this; } /** * Alias to andSet(). Allows the natural English construct * - set('foo', 'bar')->andReturn('bar') * * @param string $name * @param mixed $value * @return self */ public function set($name, $value) { return call_user_func_array(array($this, 'andSet'), func_get_args()); } /** * Indicates this expectation should occur zero or more times * * @return self */ public function zeroOrMoreTimes() { $this->atLeast()->never(); } /** * Indicates the number of times this expectation should occur * * @param int $limit * @throws \InvalidArgumentException * @return self */ public function times($limit = null) { if (is_null($limit)) { return $this; } if (!is_int($limit)) { throw new \InvalidArgumentException('The passed Times limit should be an integer value'); } $this->_countValidators[$this->_countValidatorClass] = new $this->_countValidatorClass($this, $limit); if ('Mockery\CountValidator\Exact' !== $this->_countValidatorClass) { $this->_countValidatorClass = 'Mockery\CountValidator\Exact'; unset($this->_countValidators[$this->_countValidatorClass]); } return $this; } /** * Indicates that this expectation is never expected to be called * * @return self */ public function never() { return $this->times(0); } /** * Indicates that this expectation is expected exactly once * * @return self */ public function once() { return $this->times(1); } /** * Indicates that this expectation is expected exactly twice * * @return self */ public function twice() { return $this->times(2); } /** * Sets next count validator to the AtLeast instance * * @return self */ public function atLeast() { $this->_countValidatorClass = 'Mockery\CountValidator\AtLeast'; return $this; } /** * Sets next count validator to the AtMost instance * * @return self */ public function atMost() { $this->_countValidatorClass = 'Mockery\CountValidator\AtMost'; return $this; } /** * Shorthand for setting minimum and maximum constraints on call counts * * @param int $minimum * @param int $maximum */ public function between($minimum, $maximum) { return $this->atLeast()->times($minimum)->atMost()->times($maximum); } /** * Set the exception message * * @param string $message * @return $this */ public function because($message) { $this->_because = $message; return $this; } /** * Indicates that this expectation must be called in a specific given order * * @param string $group Name of the ordered group * @return self */ public function ordered($group = null) { if ($this->_globally) { $this->_globalOrderNumber = $this->_defineOrdered($group, $this->_mock->mockery_getContainer()); } else { $this->_orderNumber = $this->_defineOrdered($group, $this->_mock); } $this->_globally = false; return $this; } /** * Indicates call order should apply globally * * @return self */ public function globally() { $this->_globally = true; return $this; } /** * Setup the ordering tracking on the mock or mock container * * @param string $group * @param object $ordering * @return int */ protected function _defineOrdered($group, $ordering) { $groups = $ordering->mockery_getGroups(); if (is_null($group)) { $result = $ordering->mockery_allocateOrder(); } elseif (isset($groups[$group])) { $result = $groups[$group]; } else { $result = $ordering->mockery_allocateOrder(); $ordering->mockery_setGroup($group, $result); } return $result; } /** * Return order number * * @return int */ public function getOrderNumber() { return $this->_orderNumber; } /** * Mark this expectation as being a default * * @return self */ public function byDefault() { $director = $this->_mock->mockery_getExpectationsFor($this->_name); if (!empty($director)) { $director->makeExpectationDefault($this); } return $this; } /** * Return the parent mock of the expectation * * @return \Mockery\LegacyMockInterface|\Mockery\MockInterface */ public function getMock() { return $this->_mock; } /** * Flag this expectation as calling the original class method with the * any provided arguments instead of using a return value queue. * * @return self */ public function passthru() { if ($this->_mock instanceof Mock) { throw new Exception( 'Mock Objects not created from a loaded/existing class are ' . 'incapable of passing method calls through to a parent class' ); } $this->_passthru = true; return $this; } /** * Cloning logic * */ public function __clone() { $newValidators = array(); $countValidators = $this->_countValidators; foreach ($countValidators as $validator) { $newValidators[] = clone $validator; } $this->_countValidators = $newValidators; } public function getName() { return $this->_name; } public function getExceptionMessage() { return $this->_because; } } Mockery/Container.php 0000644 00000036366 15107542733 0010635 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Mockery\Generator\Generator; use Mockery\Generator\MockConfigurationBuilder; use Mockery\Loader\Loader as LoaderInterface; class Container { const BLOCKS = \Mockery::BLOCKS; /** * Store of mock objects * * @var array */ protected $_mocks = array(); /** * Order number of allocation * * @var int */ protected $_allocatedOrder = 0; /** * Current ordered number * * @var int */ protected $_currentOrder = 0; /** * Ordered groups * * @var array */ protected $_groups = array(); /** * @var Generator */ protected $_generator; /** * @var LoaderInterface */ protected $_loader; /** * @var array */ protected $_namedMocks = array(); public function __construct(Generator $generator = null, LoaderInterface $loader = null) { $this->_generator = $generator ?: \Mockery::getDefaultGenerator(); $this->_loader = $loader ?: \Mockery::getDefaultLoader(); } /** * Generates a new mock object for this container * * I apologies in advance for this. A God Method just fits the API which * doesn't require differentiating between classes, interfaces, abstracts, * names or partials - just so long as it's something that can be mocked. * I'll refactor it one day so it's easier to follow. * * @param array ...$args * * @return Mock * @throws Exception\RuntimeException */ public function mock(...$args) { $expectationClosure = null; $quickdefs = array(); $constructorArgs = null; $blocks = array(); $class = null; if (count($args) > 1) { $finalArg = end($args); reset($args); if (is_callable($finalArg) && is_object($finalArg)) { $expectationClosure = array_pop($args); } } $builder = new MockConfigurationBuilder(); foreach ($args as $k => $arg) { if ($arg instanceof MockConfigurationBuilder) { $builder = $arg; unset($args[$k]); } } reset($args); $builder->setParameterOverrides(\Mockery::getConfiguration()->getInternalClassMethodParamMaps()); $builder->setConstantsMap(\Mockery::getConfiguration()->getConstantsMap()); while (count($args) > 0) { $arg = array_shift($args); // check for multiple interfaces if (is_string($arg)) { foreach (explode('|', $arg) as $type) { if ($arg === 'null') { // skip PHP 8 'null's } elseif (strpos($type, ',') && !strpos($type, ']')) { $interfaces = explode(',', str_replace(' ', '', $type)); $builder->addTargets($interfaces); } elseif (substr($type, 0, 6) == 'alias:') { $type = str_replace('alias:', '', $type); $builder->addTarget('stdClass'); $builder->setName($type); } elseif (substr($type, 0, 9) == 'overload:') { $type = str_replace('overload:', '', $type); $builder->setInstanceMock(true); $builder->addTarget('stdClass'); $builder->setName($type); } elseif (substr($type, strlen($type)-1, 1) == ']') { $parts = explode('[', $type); if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) { throw new \Mockery\Exception('Can only create a partial mock from' . ' an existing class or interface'); } $class = $parts[0]; $parts[1] = str_replace(' ', '', $parts[1]); $partialMethods = array_filter(explode(',', strtolower(rtrim($parts[1], ']')))); $builder->addTarget($class); foreach ($partialMethods as $partialMethod) { if ($partialMethod[0] === '!') { $builder->addBlackListedMethod(substr($partialMethod, 1)); continue; } $builder->addWhiteListedMethod($partialMethod); } } elseif (class_exists($type, true) || interface_exists($type, true) || trait_exists($type, true)) { $builder->addTarget($type); } elseif (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($type, true) && !interface_exists($type, true))) { throw new \Mockery\Exception("Mockery can't find '$type' so can't mock it"); } else { if (!$this->isValidClassName($type)) { throw new \Mockery\Exception('Class name contains invalid characters'); } $builder->addTarget($type); } break; // unions are "sum" types and not "intersections", and so we must only process the first part } } elseif (is_object($arg)) { $builder->addTarget($arg); } elseif (is_array($arg)) { if (!empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) { // if associative array if (array_key_exists(self::BLOCKS, $arg)) { $blocks = $arg[self::BLOCKS]; } unset($arg[self::BLOCKS]); $quickdefs = $arg; } else { $constructorArgs = $arg; } } else { throw new \Mockery\Exception( 'Unable to parse arguments sent to ' . get_class($this) . '::mock()' ); } } $builder->addBlackListedMethods($blocks); if (!is_null($constructorArgs)) { $builder->addBlackListedMethod("__construct"); // we need to pass through } else { $builder->setMockOriginalDestructor(true); } if (!empty($partialMethods) && $constructorArgs === null) { $constructorArgs = array(); } $config = $builder->getMockConfiguration(); $this->checkForNamedMockClashes($config); $def = $this->getGenerator()->generate($config); if (class_exists($def->getClassName(), $attemptAutoload = false)) { $rfc = new \ReflectionClass($def->getClassName()); if (!$rfc->implementsInterface("Mockery\LegacyMockInterface")) { throw new \Mockery\Exception\RuntimeException("Could not load mock {$def->getClassName()}, class already exists"); } } $this->getLoader()->load($def); $mock = $this->_getInstance($def->getClassName(), $constructorArgs); $mock->mockery_init($this, $config->getTargetObject(), $config->isInstanceMock()); if (!empty($quickdefs)) { if (\Mockery::getConfiguration()->getQuickDefinitions()->shouldBeCalledAtLeastOnce()) { $mock->shouldReceive($quickdefs)->atLeast()->once(); } else { $mock->shouldReceive($quickdefs)->byDefault(); } } if (!empty($expectationClosure)) { $expectationClosure($mock); } $this->rememberMock($mock); return $mock; } public function instanceMock() { } public function getLoader() { return $this->_loader; } public function getGenerator() { return $this->_generator; } /** * @param string $method * @param string $parent * @return string|null */ public function getKeyOfDemeterMockFor($method, $parent) { $keys = array_keys($this->_mocks); $match = preg_grep("/__demeter_" . md5($parent) . "_{$method}$/", $keys); if (count($match) == 1) { $res = array_values($match); if (count($res) > 0) { return $res[0]; } } return null; } /** * @return array */ public function getMocks() { return $this->_mocks; } /** * Tear down tasks for this container * * @throws \Exception * @return void */ public function mockery_teardown() { try { $this->mockery_verify(); } catch (\Exception $e) { $this->mockery_close(); throw $e; } } /** * Verify the container mocks * * @return void */ public function mockery_verify() { foreach ($this->_mocks as $mock) { $mock->mockery_verify(); } } /** * Retrieves all exceptions thrown by mocks * * @return array */ public function mockery_thrownExceptions() { $e = []; foreach ($this->_mocks as $mock) { $e = array_merge($e, $mock->mockery_thrownExceptions()); } return $e; } /** * Reset the container to its original state * * @return void */ public function mockery_close() { foreach ($this->_mocks as $mock) { $mock->mockery_teardown(); } $this->_mocks = array(); } /** * Fetch the next available allocation order number * * @return int */ public function mockery_allocateOrder() { $this->_allocatedOrder += 1; return $this->_allocatedOrder; } /** * Set ordering for a group * * @param mixed $group * @param int $order */ public function mockery_setGroup($group, $order) { $this->_groups[$group] = $order; } /** * Fetch array of ordered groups * * @return array */ public function mockery_getGroups() { return $this->_groups; } /** * Set current ordered number * * @param int $order * @return int The current order number that was set */ public function mockery_setCurrentOrder($order) { $this->_currentOrder = $order; return $this->_currentOrder; } /** * Get current ordered number * * @return int */ public function mockery_getCurrentOrder() { return $this->_currentOrder; } /** * Validate the current mock's ordering * * @param string $method * @param int $order * @throws \Mockery\Exception * @return void */ public function mockery_validateOrder($method, $order, \Mockery\LegacyMockInterface $mock) { if ($order < $this->_currentOrder) { $exception = new \Mockery\Exception\InvalidOrderException( 'Method ' . $method . ' called out of order: expected order ' . $order . ', was ' . $this->_currentOrder ); $exception->setMock($mock) ->setMethodName($method) ->setExpectedOrder($order) ->setActualOrder($this->_currentOrder); throw $exception; } $this->mockery_setCurrentOrder($order); } /** * Gets the count of expectations on the mocks * * @return int */ public function mockery_getExpectationCount() { $count = 0; foreach ($this->_mocks as $mock) { $count += $mock->mockery_getExpectationCount(); } return $count; } /** * Store a mock and set its container reference * * @param \Mockery\Mock $mock * @return \Mockery\LegacyMockInterface|\Mockery\MockInterface */ public function rememberMock(\Mockery\LegacyMockInterface $mock) { if (!isset($this->_mocks[get_class($mock)])) { $this->_mocks[get_class($mock)] = $mock; } else { /** * This condition triggers for an instance mock where origin mock * is already remembered */ $this->_mocks[] = $mock; } return $mock; } /** * Retrieve the last remembered mock object, which is the same as saying * retrieve the current mock being programmed where you have yet to call * mock() to change it - thus why the method name is "self" since it will be * be used during the programming of the same mock. * * @return \Mockery\Mock */ public function self() { $mocks = array_values($this->_mocks); $index = count($mocks) - 1; return $mocks[$index]; } /** * Return a specific remembered mock according to the array index it * was stored to in this container instance * * @return \Mockery\Mock */ public function fetchMock($reference) { if (isset($this->_mocks[$reference])) { return $this->_mocks[$reference]; } } protected function _getInstance($mockName, $constructorArgs = null) { if ($constructorArgs !== null) { $r = new \ReflectionClass($mockName); return $r->newInstanceArgs($constructorArgs); } try { $instantiator = new Instantiator(); $instance = $instantiator->instantiate($mockName); } catch (\Exception $ex) { $internalMockName = $mockName . '_Internal'; if (!class_exists($internalMockName)) { eval("class $internalMockName extends $mockName {" . 'public function __construct() {}' . '}'); } $instance = new $internalMockName(); } return $instance; } protected function checkForNamedMockClashes($config) { $name = $config->getName(); if (!$name) { return; } $hash = $config->getHash(); if (isset($this->_namedMocks[$name])) { if ($hash !== $this->_namedMocks[$name]) { throw new \Mockery\Exception( "The mock named '$name' has been already defined with a different mock configuration" ); } } $this->_namedMocks[$name] = $hash; } /** * see http://php.net/manual/en/language.oop5.basic.php * @param string $className * @return bool */ public function isValidClassName($className) { $pos = strpos($className, '\\'); if ($pos === 0) { $className = substr($className, 1); // remove the first backslash } // all the namespaces and class name should match the regex $invalidNames = array_filter(explode('\\', $className), function ($name) { return !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name); }); return empty($invalidNames); } } Mockery/Undefined.php 0000644 00000001415 15107542733 0010577 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class Undefined { /** * Call capturing to merely return this same object. * * @param string $method * @param array $args * @return self */ public function __call($method, array $args) { return $this; } /** * Return a string, avoiding E_RECOVERABLE_ERROR * * @return string */ public function __toString() { return __CLASS__ . ":" . spl_object_hash($this); } } Mockery/Exception.php 0000644 00000000717 15107542733 0010640 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Mockery\Exception\MockeryExceptionInterface; class Exception extends \UnexpectedValueException implements MockeryExceptionInterface { } Mockery/Exception/BadMethodCallException.php 0000644 00000001445 15107542733 0015141 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; class BadMethodCallException extends \BadMethodCallException implements MockeryExceptionInterface { private $dismissed = false; public function dismiss() { $this->dismissed = true; // we sometimes stack them if ($this->getPrevious() && $this->getPrevious() instanceof BadMethodCallException) { $this->getPrevious()->dismiss(); } } public function dismissed() { return $this->dismissed; } } Mockery/Exception/InvalidOrderException.php 0000644 00000002605 15107542733 0015077 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; use Mockery; class InvalidOrderException extends Mockery\Exception { protected $method = null; protected $expected = 0; protected $actual = null; protected $mockObject = null; public function setMock(Mockery\LegacyMockInterface $mock) { $this->mockObject = $mock; return $this; } public function setMethodName($name) { $this->method = $name; return $this; } public function setActualOrder($count) { $this->actual = $count; return $this; } public function setExpectedOrder($count) { $this->expected = $count; return $this; } public function getMock() { return $this->mockObject; } public function getMethodName() { return $this->method; } public function getActualOrder() { return $this->actual; } public function getExpectedOrder() { return $this->expected; } public function getMockName() { return $this->getMock()->mockery_getName(); } } Mockery/Exception/RuntimeException.php 0000644 00000000637 15107542733 0014143 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; class RuntimeException extends \Exception implements MockeryExceptionInterface { } Mockery/Exception/MockeryExceptionInterface.php 0000644 00000000660 15107542733 0015746 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ declare(strict_types=1); namespace Mockery\Exception; use Throwable; interface MockeryExceptionInterface extends Throwable { } Mockery/Exception/NoMatchingExpectationException.php 0000644 00000002265 15107542733 0016752 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; use Mockery; class NoMatchingExpectationException extends Mockery\Exception { protected $method = null; protected $actual = array(); protected $mockObject = null; public function setMock(Mockery\LegacyMockInterface $mock) { $this->mockObject = $mock; return $this; } public function setMethodName($name) { $this->method = $name; return $this; } public function setActualArguments($count) { $this->actual = $count; return $this; } public function getMock() { return $this->mockObject; } public function getMethodName() { return $this->method; } public function getActualArguments() { return $this->actual; } public function getMockName() { return $this->getMock()->mockery_getName(); } } Mockery/Exception/InvalidArgumentException.php 0000644 00000000666 15107542733 0015613 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; class InvalidArgumentException extends \InvalidArgumentException implements MockeryExceptionInterface { } Mockery/Exception/InvalidCountException.php 0000644 00000003650 15107542733 0015115 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Exception; use Mockery; use Mockery\Exception\RuntimeException; class InvalidCountException extends Mockery\CountValidator\Exception { protected $method = null; protected $expected = 0; protected $expectedComparative = '<='; protected $actual = null; protected $mockObject = null; public function setMock(Mockery\LegacyMockInterface $mock) { $this->mockObject = $mock; return $this; } public function setMethodName($name) { $this->method = $name; return $this; } public function setActualCount($count) { $this->actual = $count; return $this; } public function setExpectedCount($count) { $this->expected = $count; return $this; } public function setExpectedCountComparative($comp) { if (!in_array($comp, array('=', '>', '<', '>=', '<='))) { throw new RuntimeException( 'Illegal comparative for expected call counts set: ' . $comp ); } $this->expectedComparative = $comp; return $this; } public function getMock() { return $this->mockObject; } public function getMethodName() { return $this->method; } public function getActualCount() { return $this->actual; } public function getExpectedCount() { return $this->expected; } public function getMockName() { return $this->getMock()->mockery_getName(); } public function getExpectedCountComparative() { return $this->expectedComparative; } } Mockery/VerificationDirector.php 0000644 00000005264 15107542733 0013022 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class VerificationDirector { private $receivedMethodCalls; private $expectation; public function __construct(ReceivedMethodCalls $receivedMethodCalls, VerificationExpectation $expectation) { $this->receivedMethodCalls = $receivedMethodCalls; $this->expectation = $expectation; } public function verify() { return $this->receivedMethodCalls->verify($this->expectation); } public function with(...$args) { return $this->cloneApplyAndVerify("with", $args); } public function withArgs($args) { return $this->cloneApplyAndVerify("withArgs", array($args)); } public function withNoArgs() { return $this->cloneApplyAndVerify("withNoArgs", array()); } public function withAnyArgs() { return $this->cloneApplyAndVerify("withAnyArgs", array()); } public function times($limit = null) { return $this->cloneWithoutCountValidatorsApplyAndVerify("times", array($limit)); } public function once() { return $this->cloneWithoutCountValidatorsApplyAndVerify("once", array()); } public function twice() { return $this->cloneWithoutCountValidatorsApplyAndVerify("twice", array()); } public function atLeast() { return $this->cloneWithoutCountValidatorsApplyAndVerify("atLeast", array()); } public function atMost() { return $this->cloneWithoutCountValidatorsApplyAndVerify("atMost", array()); } public function between($minimum, $maximum) { return $this->cloneWithoutCountValidatorsApplyAndVerify("between", array($minimum, $maximum)); } protected function cloneWithoutCountValidatorsApplyAndVerify($method, $args) { $expectation = clone $this->expectation; $expectation->clearCountValidators(); call_user_func_array(array($expectation, $method), $args); $director = new VerificationDirector($this->receivedMethodCalls, $expectation); $director->verify(); return $director; } protected function cloneApplyAndVerify($method, $args) { $expectation = clone $this->expectation; call_user_func_array(array($expectation, $method), $args); $director = new VerificationDirector($this->receivedMethodCalls, $expectation); $director->verify(); return $director; } } Mockery/Mock.php 0000644 00000070604 15107542733 0007575 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Mockery\Exception\BadMethodCallException; use Mockery\ExpectsHigherOrderMessage; use Mockery\HigherOrderMessage; use Mockery\LegacyMockInterface; use Mockery\MockInterface; use Mockery\Reflector; #[\AllowDynamicProperties] class Mock implements MockInterface { /** * Stores an array of all expectation directors for this mock * * @var array */ protected $_mockery_expectations = array(); /** * Stores an initial number of expectations that can be manipulated * while using the getter method. * * @var int */ protected $_mockery_expectations_count = 0; /** * Flag to indicate whether we can ignore method calls missing from our * expectations * * @var bool */ protected $_mockery_ignoreMissing = false; /** * Flag to indicate whether we want to set the ignoreMissing flag on * mocks generated form this calls to this one * * @var bool */ protected $_mockery_ignoreMissingRecursive = false; /** * Flag to indicate whether we can defer method calls missing from our * expectations * * @var bool */ protected $_mockery_deferMissing = false; /** * Flag to indicate whether this mock was verified * * @var bool */ protected $_mockery_verified = false; /** * Given name of the mock * * @var string */ protected $_mockery_name = null; /** * Order number of allocation * * @var int */ protected $_mockery_allocatedOrder = 0; /** * Current ordered number * * @var int */ protected $_mockery_currentOrder = 0; /** * Ordered groups * * @var array */ protected $_mockery_groups = array(); /** * Mock container containing this mock object * * @var \Mockery\Container */ protected $_mockery_container = null; /** * Instance of a core object on which methods are called in the event * it has been set, and an expectation for one of the object's methods * does not exist. This implements a simple partial mock proxy system. * * @var object */ protected $_mockery_partial = null; /** * Flag to indicate we should ignore all expectations temporarily. Used * mainly to prevent expectation matching when in the middle of a mock * object recording session. * * @var bool */ protected $_mockery_disableExpectationMatching = false; /** * Stores all stubbed public methods separate from any on-object public * properties that may exist. * * @var array */ protected $_mockery_mockableProperties = array(); /** * @var array */ protected $_mockery_mockableMethods = array(); /** * Just a local cache for this mock's target's methods * * @var \ReflectionMethod[] */ protected static $_mockery_methods; protected $_mockery_allowMockingProtectedMethods = false; protected $_mockery_receivedMethodCalls; /** * If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods * @var mixed */ protected $_mockery_defaultReturnValue = null; /** * Tracks internally all the bad method call exceptions that happened during runtime * * @var array */ protected $_mockery_thrownExceptions = []; protected $_mockery_instanceMock = true; /** * We want to avoid constructors since class is copied to Generator.php * for inclusion on extending class definitions. * * @param \Mockery\Container $container * @param object $partialObject * @param bool $instanceMock * @return void */ public function mockery_init(\Mockery\Container $container = null, $partialObject = null, $instanceMock = true) { if (is_null($container)) { $container = new \Mockery\Container(); } $this->_mockery_container = $container; if (!is_null($partialObject)) { $this->_mockery_partial = $partialObject; } if (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) { foreach ($this->mockery_getMethods() as $method) { if ($method->isPublic()) { $this->_mockery_mockableMethods[] = $method->getName(); } } } $this->_mockery_instanceMock = $instanceMock; } /** * Set expected method calls * * @param string ...$methodNames one or many methods that are expected to be called in this mock * * @return \Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function shouldReceive(...$methodNames) { if (count($methodNames) === 0) { return new HigherOrderMessage($this, "shouldReceive"); } foreach ($methodNames as $method) { if ("" == $method) { throw new \InvalidArgumentException("Received empty method name"); } } $self = $this; $allowMockingProtectedMethods = $this->_mockery_allowMockingProtectedMethods; $lastExpectation = \Mockery::parseShouldReturnArgs( $this, $methodNames, function ($method) use ($self, $allowMockingProtectedMethods) { $rm = $self->mockery_getMethod($method); if ($rm) { if ($rm->isPrivate()) { throw new \InvalidArgumentException("$method() cannot be mocked as it is a private method"); } if (!$allowMockingProtectedMethods && $rm->isProtected()) { throw new \InvalidArgumentException("$method() cannot be mocked as it is a protected method and mocking protected methods is not enabled for the currently used mock object. Use shouldAllowMockingProtectedMethods() to enable mocking of protected methods."); } } $director = $self->mockery_getExpectationsFor($method); if (!$director) { $director = new \Mockery\ExpectationDirector($method, $self); $self->mockery_setExpectationsFor($method, $director); } $expectation = new \Mockery\Expectation($self, $method); $director->addExpectation($expectation); return $expectation; } ); return $lastExpectation; } // start method allows /** * @param mixed $something String method name or map of method => return * @return self|\Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function allows($something = []) { if (is_string($something)) { return $this->shouldReceive($something); } if (empty($something)) { return $this->shouldReceive(); } foreach ($something as $method => $returnValue) { $this->shouldReceive($method)->andReturn($returnValue); } return $this; } // end method allows // start method expects /** /** * @param mixed $something String method name (optional) * @return \Mockery\ExpectationInterface|\Mockery\Expectation|ExpectsHigherOrderMessage */ public function expects($something = null) { if (is_string($something)) { return $this->shouldReceive($something)->once(); } return new ExpectsHigherOrderMessage($this); } // end method expects /** * Shortcut method for setting an expectation that a method should not be called. * * @param string ...$methodNames one or many methods that are expected not to be called in this mock * @return \Mockery\ExpectationInterface|\Mockery\Expectation|\Mockery\HigherOrderMessage */ public function shouldNotReceive(...$methodNames) { if (count($methodNames) === 0) { return new HigherOrderMessage($this, "shouldNotReceive"); } $expectation = call_user_func_array(array($this, 'shouldReceive'), $methodNames); $expectation->never(); return $expectation; } /** * Allows additional methods to be mocked that do not explicitly exist on mocked class * @param String $method name of the method to be mocked * @return Mock */ public function shouldAllowMockingMethod($method) { $this->_mockery_mockableMethods[] = $method; return $this; } /** * Set mock to ignore unexpected methods and return Undefined class * @param mixed $returnValue the default return value for calls to missing functions on this mock * @param bool $recursive Specify if returned mocks should also have shouldIgnoreMissing set * @return static */ public function shouldIgnoreMissing($returnValue = null, $recursive = false) { $this->_mockery_ignoreMissing = true; $this->_mockery_ignoreMissingRecursive = $recursive; $this->_mockery_defaultReturnValue = $returnValue; return $this; } public function asUndefined() { $this->_mockery_ignoreMissing = true; $this->_mockery_defaultReturnValue = new \Mockery\Undefined(); return $this; } /** * @return static */ public function shouldAllowMockingProtectedMethods() { if (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) { foreach ($this->mockery_getMethods() as $method) { if ($method->isProtected()) { $this->_mockery_mockableMethods[] = $method->getName(); } } } $this->_mockery_allowMockingProtectedMethods = true; return $this; } /** * Set mock to defer unexpected methods to it's parent * * This is particularly useless for this class, as it doesn't have a parent, * but included for completeness * * @deprecated 2.0.0 Please use makePartial() instead * * @return static */ public function shouldDeferMissing() { return $this->makePartial(); } /** * Set mock to defer unexpected methods to it's parent * * It was an alias for shouldDeferMissing(), which will be removed * in 2.0.0. * * @return static */ public function makePartial() { $this->_mockery_deferMissing = true; return $this; } /** * In the event shouldReceive() accepting one or more methods/returns, * this method will switch them from normal expectations to default * expectations * * @return self */ public function byDefault() { foreach ($this->_mockery_expectations as $director) { $exps = $director->getExpectations(); foreach ($exps as $exp) { $exp->byDefault(); } } return $this; } /** * Capture calls to this mock */ public function __call($method, array $args) { return $this->_mockery_handleMethodCall($method, $args); } public static function __callStatic($method, array $args) { return self::_mockery_handleStaticMethodCall($method, $args); } /** * Forward calls to this magic method to the __call method */ public function __toString() { return $this->__call('__toString', array()); } /** * Iterate across all expectation directors and validate each * * @throws \Mockery\CountValidator\Exception * @return void */ public function mockery_verify() { if ($this->_mockery_verified) { return; } if (isset($this->_mockery_ignoreVerification) && $this->_mockery_ignoreVerification == true) { return; } $this->_mockery_verified = true; foreach ($this->_mockery_expectations as $director) { $director->verify(); } } /** * Gets a list of exceptions thrown by this mock * * @return array */ public function mockery_thrownExceptions() { return $this->_mockery_thrownExceptions; } /** * Tear down tasks for this mock * * @return void */ public function mockery_teardown() { } /** * Fetch the next available allocation order number * * @return int */ public function mockery_allocateOrder() { $this->_mockery_allocatedOrder += 1; return $this->_mockery_allocatedOrder; } /** * Set ordering for a group * * @param mixed $group * @param int $order */ public function mockery_setGroup($group, $order) { $this->_mockery_groups[$group] = $order; } /** * Fetch array of ordered groups * * @return array */ public function mockery_getGroups() { return $this->_mockery_groups; } /** * Set current ordered number * * @param int $order */ public function mockery_setCurrentOrder($order) { $this->_mockery_currentOrder = $order; return $this->_mockery_currentOrder; } /** * Get current ordered number * * @return int */ public function mockery_getCurrentOrder() { return $this->_mockery_currentOrder; } /** * Validate the current mock's ordering * * @param string $method * @param int $order * @throws \Mockery\Exception * @return void */ public function mockery_validateOrder($method, $order) { if ($order < $this->_mockery_currentOrder) { $exception = new \Mockery\Exception\InvalidOrderException( 'Method ' . __CLASS__ . '::' . $method . '()' . ' called out of order: expected order ' . $order . ', was ' . $this->_mockery_currentOrder ); $exception->setMock($this) ->setMethodName($method) ->setExpectedOrder($order) ->setActualOrder($this->_mockery_currentOrder); throw $exception; } $this->mockery_setCurrentOrder($order); } /** * Gets the count of expectations for this mock * * @return int */ public function mockery_getExpectationCount() { $count = $this->_mockery_expectations_count; foreach ($this->_mockery_expectations as $director) { $count += $director->getExpectationCount(); } return $count; } /** * Return the expectations director for the given method * * @var string $method * @return \Mockery\ExpectationDirector|null */ public function mockery_setExpectationsFor($method, \Mockery\ExpectationDirector $director) { $this->_mockery_expectations[$method] = $director; } /** * Return the expectations director for the given method * * @var string $method * @return \Mockery\ExpectationDirector|null */ public function mockery_getExpectationsFor($method) { if (isset($this->_mockery_expectations[$method])) { return $this->_mockery_expectations[$method]; } } /** * Find an expectation matching the given method and arguments * * @var string $method * @var array $args * @return \Mockery\Expectation|null */ public function mockery_findExpectation($method, array $args) { if (!isset($this->_mockery_expectations[$method])) { return null; } $director = $this->_mockery_expectations[$method]; return $director->findExpectation($args); } /** * Return the container for this mock * * @return \Mockery\Container */ public function mockery_getContainer() { return $this->_mockery_container; } /** * Return the name for this mock * * @return string */ public function mockery_getName() { return __CLASS__; } /** * @return array */ public function mockery_getMockableProperties() { return $this->_mockery_mockableProperties; } public function __isset($name) { if (false === stripos($name, '_mockery_') && get_parent_class($this) && method_exists(get_parent_class($this), '__isset')) { return call_user_func(get_parent_class($this) . '::__isset', $name); } return false; } public function mockery_getExpectations() { return $this->_mockery_expectations; } /** * Calls a parent class method and returns the result. Used in a passthru * expectation where a real return value is required while still taking * advantage of expectation matching and call count verification. * * @param string $name * @param array $args * @return mixed */ public function mockery_callSubjectMethod($name, array $args) { if (!method_exists($this, $name) && get_parent_class($this) && method_exists(get_parent_class($this), '__call')) { return call_user_func(get_parent_class($this) . '::__call', $name, $args); } return call_user_func_array(get_parent_class($this) . '::' . $name, $args); } /** * @return string[] */ public function mockery_getMockableMethods() { return $this->_mockery_mockableMethods; } /** * @return bool */ public function mockery_isAnonymous() { $rfc = new \ReflectionClass($this); // PHP 8 has Stringable interface $interfaces = array_filter($rfc->getInterfaces(), function ($i) { return $i->getName() !== 'Stringable'; }); return false === $rfc->getParentClass() && 2 === count($interfaces); } public function mockery_isInstance() { return $this->_mockery_instanceMock; } public function __wakeup() { /** * This does not add __wakeup method support. It's a blind method and any * expected __wakeup work will NOT be performed. It merely cuts off * annoying errors where a __wakeup exists but is not essential when * mocking */ } public function __destruct() { /** * Overrides real class destructor in case if class was created without original constructor */ } public function mockery_getMethod($name) { foreach ($this->mockery_getMethods() as $method) { if ($method->getName() == $name) { return $method; } } return null; } /** * @param string $name Method name. * * @return mixed Generated return value based on the declared return value of the named method. */ public function mockery_returnValueForMethod($name) { $rm = $this->mockery_getMethod($name); if ($rm === null) { return null; } $returnType = Reflector::getSimplestReturnType($rm); switch ($returnType) { case null: return null; case 'string': return ''; case 'int': return 0; case 'float': return 0.0; case 'bool': return false; case 'true': return true; case 'false': return false; case 'array': case 'iterable': return []; case 'callable': case '\Closure': return function () { }; case '\Traversable': case '\Generator': $generator = function () { yield; }; return $generator(); case 'void': return null; case 'static': return $this; case 'object': $mock = \Mockery::mock(); if ($this->_mockery_ignoreMissingRecursive) { $mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true); } return $mock; default: $mock = \Mockery::mock($returnType); if ($this->_mockery_ignoreMissingRecursive) { $mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true); } return $mock; } } public function shouldHaveReceived($method = null, $args = null) { if ($method === null) { return new HigherOrderMessage($this, "shouldHaveReceived"); } $expectation = new \Mockery\VerificationExpectation($this, $method); if (null !== $args) { $expectation->withArgs($args); } $expectation->atLeast()->once(); $director = new \Mockery\VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation); $this->_mockery_expectations_count++; $director->verify(); return $director; } public function shouldHaveBeenCalled() { return $this->shouldHaveReceived("__invoke"); } public function shouldNotHaveReceived($method = null, $args = null) { if ($method === null) { return new HigherOrderMessage($this, "shouldNotHaveReceived"); } $expectation = new \Mockery\VerificationExpectation($this, $method); if (null !== $args) { $expectation->withArgs($args); } $expectation->never(); $director = new \Mockery\VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation); $this->_mockery_expectations_count++; $director->verify(); return null; } public function shouldNotHaveBeenCalled(array $args = null) { return $this->shouldNotHaveReceived("__invoke", $args); } protected static function _mockery_handleStaticMethodCall($method, array $args) { $associatedRealObject = \Mockery::fetchMock(__CLASS__); try { return $associatedRealObject->__call($method, $args); } catch (BadMethodCallException $e) { throw new BadMethodCallException( 'Static method ' . $associatedRealObject->mockery_getName() . '::' . $method . '() does not exist on this mock object', 0, $e ); } } protected function _mockery_getReceivedMethodCalls() { return $this->_mockery_receivedMethodCalls ?: $this->_mockery_receivedMethodCalls = new \Mockery\ReceivedMethodCalls(); } /** * Called when an instance Mock was created and its constructor is getting called * * @see \Mockery\Generator\StringManipulation\Pass\InstanceMockPass * @param array $args */ protected function _mockery_constructorCalled(array $args) { if (!isset($this->_mockery_expectations['__construct']) /* _mockery_handleMethodCall runs the other checks */) { return; } $this->_mockery_handleMethodCall('__construct', $args); } protected function _mockery_findExpectedMethodHandler($method) { if (isset($this->_mockery_expectations[$method])) { return $this->_mockery_expectations[$method]; } $lowerCasedMockeryExpectations = array_change_key_case($this->_mockery_expectations, CASE_LOWER); $lowerCasedMethod = strtolower($method); if (isset($lowerCasedMockeryExpectations[$lowerCasedMethod])) { return $lowerCasedMockeryExpectations[$lowerCasedMethod]; } return null; } protected function _mockery_handleMethodCall($method, array $args) { $this->_mockery_getReceivedMethodCalls()->push(new \Mockery\MethodCall($method, $args)); $rm = $this->mockery_getMethod($method); if ($rm && $rm->isProtected() && !$this->_mockery_allowMockingProtectedMethods) { if ($rm->isAbstract()) { return; } try { $prototype = $rm->getPrototype(); if ($prototype->isAbstract()) { return; } } catch (\ReflectionException $re) { // noop - there is no hasPrototype method } return call_user_func_array(get_parent_class($this) . '::' . $method, $args); } $handler = $this->_mockery_findExpectedMethodHandler($method); if ($handler !== null && !$this->_mockery_disableExpectationMatching) { try { return $handler->call($args); } catch (\Mockery\Exception\NoMatchingExpectationException $e) { if (!$this->_mockery_ignoreMissing && !$this->_mockery_deferMissing) { throw $e; } } } if (!is_null($this->_mockery_partial) && (method_exists($this->_mockery_partial, $method) || method_exists($this->_mockery_partial, '__call')) ) { return call_user_func_array(array($this->_mockery_partial, $method), $args); } elseif ($this->_mockery_deferMissing && is_callable(get_parent_class($this) . '::' . $method) && (!$this->hasMethodOverloadingInParentClass() || (get_parent_class($this) && method_exists(get_parent_class($this), $method)))) { return call_user_func_array(get_parent_class($this) . '::' . $method, $args); } elseif ($this->_mockery_deferMissing && get_parent_class($this) && method_exists(get_parent_class($this), '__call')) { return call_user_func(get_parent_class($this) . '::__call', $method, $args); } elseif ($method == '__toString') { // __toString is special because we force its addition to the class API regardless of the // original implementation. Thus, we should always return a string rather than honor // _mockery_ignoreMissing and break the API with an error. return sprintf("%s#%s", __CLASS__, spl_object_hash($this)); } elseif ($this->_mockery_ignoreMissing) { if (\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() || (!is_null($this->_mockery_partial) && method_exists($this->_mockery_partial, $method)) || is_callable(get_parent_class($this) . '::' . $method)) { if ($this->_mockery_defaultReturnValue instanceof \Mockery\Undefined) { return call_user_func_array(array($this->_mockery_defaultReturnValue, $method), $args); } elseif (null === $this->_mockery_defaultReturnValue) { return $this->mockery_returnValueForMethod($method); } return $this->_mockery_defaultReturnValue; } } $message = 'Method ' . __CLASS__ . '::' . $method . '() does not exist on this mock object'; if (!is_null($rm)) { $message = 'Received ' . __CLASS__ . '::' . $method . '(), but no expectations were specified'; } $bmce = new BadMethodCallException($message); $this->_mockery_thrownExceptions[] = $bmce; throw $bmce; } /** * Uses reflection to get the list of all * methods within the current mock object * * @return array */ protected function mockery_getMethods() { if (static::$_mockery_methods && \Mockery::getConfiguration()->reflectionCacheEnabled()) { return static::$_mockery_methods; } if (isset($this->_mockery_partial)) { $reflected = new \ReflectionObject($this->_mockery_partial); } else { $reflected = new \ReflectionClass($this); } return static::$_mockery_methods = $reflected->getMethods(); } private function hasMethodOverloadingInParentClass() { // if there's __call any name would be callable return is_callable(get_parent_class($this) . '::aFunctionNameThatNoOneWouldEverUseInRealLife12345'); } /** * @return array */ private function getNonPublicMethods() { return array_map( function ($method) { return $method->getName(); }, array_filter($this->mockery_getMethods(), function ($method) { return !$method->isPublic(); }) ); } } Mockery/Configuration.php 0000644 00000017472 15107542733 0011517 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class Configuration { /** * Boolean assertion of whether we can mock methods which do not actually * exist for the given class or object (ignored for unreal mocks) * * @var bool */ protected $_allowMockingNonExistentMethod = true; /** * Boolean assertion of whether we ignore unnecessary mocking of methods, * i.e. when method expectations are made, set using a zeroOrMoreTimes() * constraint, and then never called. Essentially such expectations are * not required and are just taking up test space. * * @var bool */ protected $_allowMockingMethodsUnnecessarily = true; /** * @var QuickDefinitionsConfiguration */ protected $_quickDefinitionsConfiguration; /** * Parameter map for use with PHP internal classes. * * @var array */ protected $_internalClassParamMap = array(); protected $_constantsMap = array(); /** * Boolean assertion is reflection caching enabled or not. It should be * always enabled, except when using PHPUnit's --static-backup option. * * @see https://github.com/mockery/mockery/issues/268 */ protected $_reflectionCacheEnabled = true; public function __construct() { $this->_quickDefinitionsConfiguration = new QuickDefinitionsConfiguration(); } /** * Custom object formatters * * @var array */ protected $_objectFormatters = array(); /** * Default argument matchers * * @var array */ protected $_defaultMatchers = array(); /** * Set boolean to allow/prevent mocking of non-existent methods * * @param bool $flag */ public function allowMockingNonExistentMethods($flag = true) { $this->_allowMockingNonExistentMethod = (bool) $flag; } /** * Return flag indicating whether mocking non-existent methods allowed * * @return bool */ public function mockingNonExistentMethodsAllowed() { return $this->_allowMockingNonExistentMethod; } /** * Set boolean to allow/prevent unnecessary mocking of methods * * @param bool $flag * * @deprecated since 1.4.0 */ public function allowMockingMethodsUnnecessarily($flag = true) { @trigger_error(sprintf("The %s method is deprecated and will be removed in a future version of Mockery", __METHOD__), E_USER_DEPRECATED); $this->_allowMockingMethodsUnnecessarily = (bool) $flag; } /** * Return flag indicating whether mocking non-existent methods allowed * * @return bool * * @deprecated since 1.4.0 */ public function mockingMethodsUnnecessarilyAllowed() { @trigger_error(sprintf("The %s method is deprecated and will be removed in a future version of Mockery", __METHOD__), E_USER_DEPRECATED); return $this->_allowMockingMethodsUnnecessarily; } /** * Set a parameter map (array of param signature strings) for the method * of an internal PHP class. * * @param string $class * @param string $method * @param array $map */ public function setInternalClassMethodParamMap($class, $method, array $map) { if (\PHP_MAJOR_VERSION > 7) { throw new \LogicException('Internal class parameter overriding is not available in PHP 8. Incompatible signatures have been reclassified as fatal errors.'); } if (!isset($this->_internalClassParamMap[strtolower($class)])) { $this->_internalClassParamMap[strtolower($class)] = array(); } $this->_internalClassParamMap[strtolower($class)][strtolower($method)] = $map; } /** * Remove all overridden parameter maps from internal PHP classes. */ public function resetInternalClassMethodParamMaps() { $this->_internalClassParamMap = array(); } /** * Get the parameter map of an internal PHP class method * * @return array|null */ public function getInternalClassMethodParamMap($class, $method) { if (isset($this->_internalClassParamMap[strtolower($class)][strtolower($method)])) { return $this->_internalClassParamMap[strtolower($class)][strtolower($method)]; } } public function getInternalClassMethodParamMaps() { return $this->_internalClassParamMap; } public function setConstantsMap(array $map) { $this->_constantsMap = $map; } public function getConstantsMap() { return $this->_constantsMap; } /** * Returns the quick definitions configuration */ public function getQuickDefinitions(): QuickDefinitionsConfiguration { return $this->_quickDefinitionsConfiguration; } /** * Disable reflection caching * * It should be always enabled, except when using * PHPUnit's --static-backup option. * * @see https://github.com/mockery/mockery/issues/268 */ public function disableReflectionCache() { $this->_reflectionCacheEnabled = false; } /** * Enable reflection caching * * It should be always enabled, except when using * PHPUnit's --static-backup option. * * @see https://github.com/mockery/mockery/issues/268 */ public function enableReflectionCache() { $this->_reflectionCacheEnabled = true; } /** * Is reflection cache enabled? */ public function reflectionCacheEnabled() { return $this->_reflectionCacheEnabled; } public function setObjectFormatter($class, $formatterCallback) { $this->_objectFormatters[$class] = $formatterCallback; } public function getObjectFormatter($class, $defaultFormatter) { $parentClass = $class; do { $classes[] = $parentClass; $parentClass = get_parent_class($parentClass); } while ($parentClass); $classesAndInterfaces = array_merge($classes, class_implements($class)); foreach ($classesAndInterfaces as $type) { if (isset($this->_objectFormatters[$type])) { return $this->_objectFormatters[$type]; } } return $defaultFormatter; } /** * @param string $class * @param string $matcherClass */ public function setDefaultMatcher($class, $matcherClass) { $isHamcrest = is_a($matcherClass, \Hamcrest\Matcher::class, true) || is_a($matcherClass, \Hamcrest_Matcher::class, true); if ( !is_a($matcherClass, \Mockery\Matcher\MatcherAbstract::class, true) && !$isHamcrest ) { throw new \InvalidArgumentException( "Matcher class must extend \Mockery\Matcher\MatcherAbstract, " . "'$matcherClass' given." ); } if ($isHamcrest) { @trigger_error('Hamcrest package has been deprecated and will be removed in 2.0', E_USER_DEPRECATED); } $this->_defaultMatchers[$class] = $matcherClass; } public function getDefaultMatcher($class) { $parentClass = $class; do { $classes[] = $parentClass; $parentClass = get_parent_class($parentClass); } while ($parentClass); $classesAndInterfaces = array_merge($classes, class_implements($class)); foreach ($classesAndInterfaces as $type) { if (isset($this->_defaultMatchers[$type])) { return $this->_defaultMatchers[$type]; } } return null; } } Mockery/QuickDefinitionsConfiguration.php 0000644 00000003364 15107542733 0014703 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class QuickDefinitionsConfiguration { private const QUICK_DEFINITIONS_MODE_MOCK_AT_LEAST_ONCE = 'QUICK_DEFINITIONS_MODE_MOCK_AT_LEAST_ONCE'; private const QUICK_DEFINITIONS_MODE_DEFAULT_EXPECTATION = 'QUICK_DEFINITIONS_MODE_DEFAULT_EXPECTATION'; /** * Defines what a quick definition should produce. * Possible options are: * - self::QUICK_DEFINITIONS_MODE_DEFAULT_EXPECTATION: in this case quick * definitions define a stub. * - self::QUICK_DEFINITIONS_MODE_MOCK_AT_LEAST_ONCE: in this case quick * definitions define a mock with an 'at least once' expectation. * * @var string */ protected $_quickDefinitionsApplicationMode = self::QUICK_DEFINITIONS_MODE_DEFAULT_EXPECTATION; /** * Returns true if quick definitions should setup a stub, returns false when * quick definitions should setup a mock with 'at least once' expectation. * When parameter $newValue is specified it sets the configuration with the * given value. */ public function shouldBeCalledAtLeastOnce(?bool $newValue = null): bool { if ($newValue !== null) { $this->_quickDefinitionsApplicationMode = $newValue ? self::QUICK_DEFINITIONS_MODE_MOCK_AT_LEAST_ONCE : self::QUICK_DEFINITIONS_MODE_DEFAULT_EXPECTATION; } return $this->_quickDefinitionsApplicationMode === self::QUICK_DEFINITIONS_MODE_MOCK_AT_LEAST_ONCE; } } Mockery/Instantiator.php 0000644 00000010174 15107542733 0011357 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; use Closure; use ReflectionClass; use UnexpectedValueException; use InvalidArgumentException; /** * This is a trimmed down version of https://github.com/doctrine/instantiator, * basically without the caching * * @author Marco Pivetta <ocramius@gmail.com> */ final class Instantiator { /** * {@inheritDoc} */ public function instantiate($className) { $factory = $this->buildFactory($className); $instance = $factory(); return $instance; } /** * Builds a {@see \Closure} capable of instantiating the given $className without * invoking its constructor. * * @param string $className * * @return Closure */ private function buildFactory($className) { $reflectionClass = $this->getReflectionClass($className); if ($this->isInstantiableViaReflection($reflectionClass)) { return function () use ($reflectionClass) { return $reflectionClass->newInstanceWithoutConstructor(); }; } $serializedString = sprintf( 'O:%d:"%s":0:{}', strlen($className), $className ); $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString); return function () use ($serializedString) { return unserialize($serializedString); }; } /** * @param string $className * * @return ReflectionClass * * @throws InvalidArgumentException */ private function getReflectionClass($className) { if (! class_exists($className)) { throw new InvalidArgumentException("Class:$className does not exist"); } $reflection = new ReflectionClass($className); if ($reflection->isAbstract()) { throw new InvalidArgumentException("Class:$className is an abstract class"); } return $reflection; } /** * @param ReflectionClass $reflectionClass * @param string $serializedString * * @throws UnexpectedValueException * * @return void */ private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString) { set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) { $msg = sprintf( 'Could not produce an instance of "%s" via un-serialization, since an error was triggered in file "%s" at line "%d"', $reflectionClass->getName(), $file, $line ); $error = new UnexpectedValueException($msg, 0, new \Exception($message, $code)); }); try { unserialize($serializedString); } catch (\Exception $exception) { restore_error_handler(); throw new UnexpectedValueException("An exception was raised while trying to instantiate an instance of \"{$reflectionClass->getName()}\" via un-serialization", 0, $exception); } restore_error_handler(); if ($error) { throw $error; } } /** * @param ReflectionClass $reflectionClass * * @return bool */ private function isInstantiableViaReflection(ReflectionClass $reflectionClass) { return ! ($reflectionClass->isInternal() && $reflectionClass->isFinal()); } /** * Verifies whether the given class is to be considered internal * * @param ReflectionClass $reflectionClass * * @return bool */ private function hasInternalAncestors(ReflectionClass $reflectionClass) { do { if ($reflectionClass->isInternal()) { return true; } } while ($reflectionClass = $reflectionClass->getParentClass()); return false; } } Mockery/MethodCall.php 0000644 00000001214 15107542733 0010707 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class MethodCall { private $method; private $args; public function __construct($method, $args) { $this->method = $method; $this->args = $args; } public function getMethod() { return $this->method; } public function getArgs() { return $this->args; } } Mockery/Generator/StringManipulation/Pass/ClassPass.php 0000644 00000002005 15107542733 0017251 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class ClassPass implements Pass { public function apply($code, MockConfiguration $config) { $target = $config->getTargetClass(); if (!$target) { return $code; } if ($target->isFinal()) { return $code; } $className = ltrim($target->getName(), "\\"); if (!class_exists($className)) { \Mockery::declareClass($className); } $code = str_replace( "implements MockInterface", "extends \\" . $className . " implements MockInterface", $code ); return $code; } } Mockery/Generator/StringManipulation/Pass/ClassAttributesPass.php 0000644 00000001672 15107542733 0021331 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class ClassAttributesPass implements Pass { public function apply($code, MockConfiguration $config) { $class = $config->getTargetClass(); if (!$class) { return $code; } /** @var array<string> $attributes */ $attributes = $class->getAttributes(); if ($attributes !== []) { return str_replace( '#[\AllowDynamicProperties]', '#[' . implode(',', $attributes) . ']', $code ); } return $code; } } Mockery/Generator/StringManipulation/Pass/RemoveUnserializeForInternalSerializableClassesPass.php 0000644 00000003052 15107542733 0027670 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; /** * Internal classes can not be instantiated with the newInstanceWithoutArgs * reflection method, so need the serialization hack. If the class also * implements Serializable, we need to replace the standard unserialize method * definition with a dummy */ class RemoveUnserializeForInternalSerializableClassesPass { const DUMMY_METHOD_DEFINITION_LEGACY = 'public function unserialize($string) {} '; const DUMMY_METHOD_DEFINITION = 'public function unserialize(string $data): void {} '; public function apply($code, MockConfiguration $config) { $target = $config->getTargetClass(); if (!$target) { return $code; } if (!$target->hasInternalAncestor() || !$target->implementsInterface("Serializable")) { return $code; } $code = $this->appendToClass($code, \PHP_VERSION_ID < 80100 ? self::DUMMY_METHOD_DEFINITION_LEGACY : self::DUMMY_METHOD_DEFINITION); return $code; } protected function appendToClass($class, $code) { $lastBrace = strrpos($class, "}"); $class = substr($class, 0, $lastBrace) . $code . "\n }\n"; return $class; } } Mockery/Generator/StringManipulation/Pass/ConstantsPass.php 0000644 00000002054 15107542733 0020164 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class ConstantsPass implements Pass { public function apply($code, MockConfiguration $config) { $cm = $config->getConstantsMap(); if (empty($cm)) { return $code; } if (!isset($cm[$config->getName()])) { return $code; } $cm = $cm[$config->getName()]; $constantsCode = ''; foreach ($cm as $constant => $value) { $constantsCode .= sprintf("\n const %s = %s;\n", $constant, var_export($value, true)); } $i = strrpos($code, '}'); $code = substr_replace($code, $constantsCode, $i); $code .= "}\n"; return $code; } } Mockery/Generator/StringManipulation/Pass/InterfacePass.php 0000644 00000002101 15107542733 0020101 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class InterfacePass implements Pass { public function apply($code, MockConfiguration $config) { foreach ($config->getTargetInterfaces() as $i) { $name = ltrim($i->getName(), "\\"); if (!interface_exists($name)) { \Mockery::declareInterface($name); } } $interfaces = array_reduce((array) $config->getTargetInterfaces(), function ($code, $i) { return $code . ", \\" . ltrim($i->getName(), "\\"); }, ""); $code = str_replace( "implements MockInterface", "implements MockInterface" . $interfaces, $code ); return $code; } } Mockery/Generator/StringManipulation/Pass/Pass.php 0000644 00000000736 15107542733 0016274 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; interface Pass { public function apply($code, MockConfiguration $config); } Mockery/Generator/StringManipulation/Pass/RemoveDestructorPass.php 0000644 00000001550 15107542733 0021524 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; /** * Remove mock's empty destructor if we tend to use original class destructor */ class RemoveDestructorPass { public function apply($code, MockConfiguration $config) { $target = $config->getTargetClass(); if (!$target) { return $code; } if (!$config->isMockOriginalDestructor()) { $code = preg_replace('/public function __destruct\(\)\s+\{.*?\}/sm', '', $code); } return $code; } } Mockery/Generator/StringManipulation/Pass/ClassNamePass.php 0000644 00000001671 15107542733 0020062 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class ClassNamePass implements Pass { public function apply($code, MockConfiguration $config) { $namespace = $config->getNamespaceName(); $namespace = ltrim($namespace, "\\"); $className = $config->getShortName(); $code = str_replace( 'namespace Mockery;', $namespace ? 'namespace ' . $namespace . ';' : '', $code ); $code = str_replace( 'class Mock', 'class ' . $className, $code ); return $code; } } Mockery/Generator/StringManipulation/Pass/RemoveBuiltinMethodsThatAreFinalPass.php 0000644 00000002475 15107542733 0024552 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; /** * The standard Mockery\Mock class includes some methods to ease mocking, such * as __wakeup, however if the target has a final __wakeup method, it can't be * mocked. This pass removes the builtin methods where they are final on the * target */ class RemoveBuiltinMethodsThatAreFinalPass { protected $methods = array( '__wakeup' => '/public function __wakeup\(\)\s+\{.*?\}/sm', '__toString' => '/public function __toString\(\)\s+(:\s+string)?\s*\{.*?\}/sm', ); public function apply($code, MockConfiguration $config) { $target = $config->getTargetClass(); if (!$target) { return $code; } foreach ($target->getMethods() as $method) { if ($method->isFinal() && isset($this->methods[$method->getName()])) { $code = preg_replace($this->methods[$method->getName()], '', $code); } } return $code; } } Mockery/Generator/StringManipulation/Pass/MethodDefinitionPass.php 0000644 00000013626 15107542733 0021450 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\Method; use Mockery\Generator\Parameter; use Mockery\Generator\MockConfiguration; class MethodDefinitionPass implements Pass { public function apply($code, MockConfiguration $config) { foreach ($config->getMethodsToMock() as $method) { if ($method->isPublic()) { $methodDef = 'public'; } elseif ($method->isProtected()) { $methodDef = 'protected'; } else { $methodDef = 'private'; } if ($method->isStatic()) { $methodDef .= ' static'; } $methodDef .= ' function '; $methodDef .= $method->returnsReference() ? ' & ' : ''; $methodDef .= $method->getName(); $methodDef .= $this->renderParams($method, $config); $methodDef .= $this->renderReturnType($method); $methodDef .= $this->renderMethodBody($method, $config); $code = $this->appendToClass($code, $methodDef); } return $code; } protected function renderParams(Method $method, $config) { $class = $method->getDeclaringClass(); if ($class->isInternal()) { $overrides = $config->getParameterOverrides(); if (isset($overrides[strtolower($class->getName())][$method->getName()])) { return '(' . implode(',', $overrides[strtolower($class->getName())][$method->getName()]) . ')'; } } $methodParams = array(); $params = $method->getParameters(); $isPhp81 = \PHP_VERSION_ID >= 80100; foreach ($params as $param) { $paramDef = $this->renderTypeHint($param); $paramDef .= $param->isPassedByReference() ? '&' : ''; $paramDef .= $param->isVariadic() ? '...' : ''; $paramDef .= '$' . $param->getName(); if (!$param->isVariadic()) { if (false !== $param->isDefaultValueAvailable()) { $defaultValue = $param->getDefaultValue(); if (is_object($defaultValue)) { $prefix = get_class($defaultValue); if ($isPhp81) { if (enum_exists($prefix)) { $prefix = var_export($defaultValue, true); } elseif ( !$param->isDefaultValueConstant() && // "Parameter #1 [ <optional> F\Q\CN $a = new \F\Q\CN(param1, param2: 2) ] preg_match( '#<optional>\s.*?\s=\snew\s(.*?)\s]$#', $param->__toString(), $matches ) === 1 ) { $prefix = 'new ' . $matches[1]; } } } else { $prefix = var_export($defaultValue, true); } $paramDef .= ' = ' . $prefix; } elseif ($param->isOptional()) { $paramDef .= ' = null'; } } $methodParams[] = $paramDef; } return '(' . implode(', ', $methodParams) . ')'; } protected function renderReturnType(Method $method) { $type = $method->getReturnType(); return $type ? sprintf(': %s', $type) : ''; } protected function appendToClass($class, $code) { $lastBrace = strrpos($class, "}"); $class = substr($class, 0, $lastBrace) . $code . "\n }\n"; return $class; } protected function renderTypeHint(Parameter $param) { $typeHint = $param->getTypeHint(); return $typeHint === null ? '' : sprintf('%s ', $typeHint); } private function renderMethodBody($method, $config) { $invoke = $method->isStatic() ? 'static::_mockery_handleStaticMethodCall' : '$this->_mockery_handleMethodCall'; $body = <<<BODY { \$argc = func_num_args(); \$argv = func_get_args(); BODY; // Fix up known parameters by reference - used func_get_args() above // in case more parameters are passed in than the function definition // says - eg varargs. $class = $method->getDeclaringClass(); $class_name = strtolower($class->getName()); $overrides = $config->getParameterOverrides(); if (isset($overrides[$class_name][$method->getName()])) { $params = array_values($overrides[$class_name][$method->getName()]); $paramCount = count($params); for ($i = 0; $i < $paramCount; ++$i) { $param = $params[$i]; if (strpos($param, '&') !== false) { $body .= <<<BODY if (\$argc > $i) { \$argv[$i] = {$param}; } BODY; } } } else { $params = array_values($method->getParameters()); $paramCount = count($params); for ($i = 0; $i < $paramCount; ++$i) { $param = $params[$i]; if (!$param->isPassedByReference()) { continue; } $body .= <<<BODY if (\$argc > $i) { \$argv[$i] =& \${$param->getName()}; } BODY; } } $body .= "\$ret = {$invoke}(__FUNCTION__, \$argv);\n"; if (! in_array($method->getReturnType(), ['never', 'void'], true)) { $body .= "return \$ret;\n"; } $body .= "}\n"; return $body; } } Mockery/Generator/StringManipulation/Pass/TraitPass.php 0000644 00000001651 15107542733 0017275 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class TraitPass implements Pass { public function apply($code, MockConfiguration $config) { $traits = $config->getTargetTraits(); if (empty($traits)) { return $code; } $useStatements = array_map(function ($trait) { return "use \\\\" . ltrim($trait->getName(), "\\") . ";"; }, $traits); $code = preg_replace( '/^{$/m', "{\n " . implode("\n ", $useStatements) . "\n", $code ); return $code; } } Mockery/Generator/StringManipulation/Pass/CallTypeHintPass.php 0000644 00000002071 15107542733 0020547 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class CallTypeHintPass implements Pass { public function apply($code, MockConfiguration $config) { if ($config->requiresCallTypeHintRemoval()) { $code = str_replace( 'public function __call($method, array $args)', 'public function __call($method, $args)', $code ); } if ($config->requiresCallStaticTypeHintRemoval()) { $code = str_replace( 'public static function __callStatic($method, array $args)', 'public static function __callStatic($method, $args)', $code ); } return $code; } } Mockery/Generator/StringManipulation/Pass/MagicMethodTypeHintsPass.php 0000644 00000012453 15107542733 0022245 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; use Mockery\Generator\TargetClassInterface; use Mockery\Generator\Method; use Mockery\Generator\Parameter; class MagicMethodTypeHintsPass implements Pass { /** * @var array $mockMagicMethods */ private $mockMagicMethods = array( '__construct', '__destruct', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__toString', '__invoke', '__set_state', '__clone', '__debugInfo' ); /** * Apply implementation. * * @param string $code * @param MockConfiguration $config * @return string */ public function apply($code, MockConfiguration $config) { $magicMethods = $this->getMagicMethods($config->getTargetClass()); foreach ($config->getTargetInterfaces() as $interface) { $magicMethods = array_merge($magicMethods, $this->getMagicMethods($interface)); } foreach ($magicMethods as $method) { $code = $this->applyMagicTypeHints($code, $method); } return $code; } /** * Returns the magic methods within the * passed DefinedTargetClass. * * @param TargetClassInterface $class * @return array */ public function getMagicMethods( TargetClassInterface $class = null ) { if (is_null($class)) { return array(); } return array_filter($class->getMethods(), function (Method $method) { return in_array($method->getName(), $this->mockMagicMethods); }); } /** * Applies type hints of magic methods from * class to the passed code. * * @param int $code * @param Method $method * @return string */ private function applyMagicTypeHints($code, Method $method) { if ($this->isMethodWithinCode($code, $method)) { $namedParameters = $this->getOriginalParameters( $code, $method ); $code = preg_replace( $this->getDeclarationRegex($method->getName()), $this->getMethodDeclaration($method, $namedParameters), $code ); } return $code; } /** * Checks if the method is declared within code. * * @param int $code * @param Method $method * @return boolean */ private function isMethodWithinCode($code, Method $method) { return preg_match( $this->getDeclarationRegex($method->getName()), $code ) == 1; } /** * Returns the method original parameters, as they're * described in the $code string. * * @param int $code * @param Method $method * @return array */ private function getOriginalParameters($code, Method $method) { $matches = []; $parameterMatches = []; preg_match( $this->getDeclarationRegex($method->getName()), $code, $matches ); if (count($matches) > 0) { preg_match_all( '/(?<=\$)(\w+)+/i', $matches[0], $parameterMatches ); } $groupMatches = end($parameterMatches); $parameterNames = is_array($groupMatches) ? $groupMatches : [$groupMatches]; return $parameterNames; } /** * Gets the declaration code, as a string, for the passed method. * * @param Method $method * @param array $namedParameters * @return string */ private function getMethodDeclaration( Method $method, array $namedParameters ) { $declaration = 'public'; $declaration .= $method->isStatic() ? ' static' : ''; $declaration .= ' function ' . $method->getName() . '('; foreach ($method->getParameters() as $index => $parameter) { $declaration .= $this->renderTypeHint($parameter); $name = isset($namedParameters[$index]) ? $namedParameters[$index] : $parameter->getName(); $declaration .= '$' . $name; $declaration .= ','; } $declaration = rtrim($declaration, ','); $declaration .= ') '; $returnType = $method->getReturnType(); if ($returnType !== null) { $declaration .= sprintf(': %s', $returnType); } return $declaration; } protected function renderTypeHint(Parameter $param) { $typeHint = $param->getTypeHint(); return $typeHint === null ? '' : sprintf('%s ', $typeHint); } /** * Returns a regex string used to match the * declaration of some method. * * @param string $methodName * @return string */ private function getDeclarationRegex($methodName) { return "/public\s+(?:static\s+)?function\s+$methodName\s*\(.*\)\s*(?=\{)/i"; } } Mockery/Generator/StringManipulation/Pass/InstanceMockPass.php 0000644 00000005064 15107542733 0020572 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\MockConfiguration; class InstanceMockPass { const INSTANCE_MOCK_CODE = <<<MOCK protected \$_mockery_ignoreVerification = true; public function __construct() { \$this->_mockery_ignoreVerification = false; \$associatedRealObject = \Mockery::fetchMock(__CLASS__); foreach (get_object_vars(\$this) as \$attr => \$val) { if (\$attr !== "_mockery_ignoreVerification" && \$attr !== "_mockery_expectations") { \$this->\$attr = \$associatedRealObject->\$attr; } } \$directors = \$associatedRealObject->mockery_getExpectations(); foreach (\$directors as \$method=>\$director) { // get the director method needed \$existingDirector = \$this->mockery_getExpectationsFor(\$method); if (!\$existingDirector) { \$existingDirector = new \Mockery\ExpectationDirector(\$method, \$this); \$this->mockery_setExpectationsFor(\$method, \$existingDirector); } \$expectations = \$director->getExpectations(); foreach (\$expectations as \$expectation) { \$clonedExpectation = clone \$expectation; \$existingDirector->addExpectation(\$clonedExpectation); } \$defaultExpectations = \$director->getDefaultExpectations(); foreach (array_reverse(\$defaultExpectations) as \$expectation) { \$clonedExpectation = clone \$expectation; \$existingDirector->addExpectation(\$clonedExpectation); \$existingDirector->makeExpectationDefault(\$clonedExpectation); } } \Mockery::getContainer()->rememberMock(\$this); \$this->_mockery_constructorCalled(func_get_args()); } MOCK; public function apply($code, MockConfiguration $config) { if ($config->isInstanceMock()) { $code = $this->appendToClass($code, static::INSTANCE_MOCK_CODE); } return $code; } protected function appendToClass($class, $code) { $lastBrace = strrpos($class, "}"); $class = substr($class, 0, $lastBrace) . $code . "\n }\n"; return $class; } } Mockery/Generator/StringManipulation/Pass/AvoidMethodClashPass.php 0000644 00000002174 15107542733 0021371 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator\StringManipulation\Pass; use Mockery\Generator\Method; use Mockery\Generator\Parameter; use Mockery\Generator\MockConfiguration; class AvoidMethodClashPass implements Pass { public function apply($code, MockConfiguration $config) { $names = array_map(function ($method) { return $method->getName(); }, $config->getMethodsToMock()); foreach (["allows", "expects"] as $method) { if (in_array($method, $names)) { $code = preg_replace( "#// start method {$method}.*// end method {$method}#ms", "", $code ); $code = str_replace(" implements MockInterface", " implements LegacyMockInterface", $code); } } return $code; } } Mockery/Generator/Method.php 0000644 00000002050 15107542733 0012040 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; use Mockery\Reflector; class Method { /** @var \ReflectionMethod */ private $method; public function __construct(\ReflectionMethod $method) { $this->method = $method; } public function __call($method, $args) { return call_user_func_array(array($this->method, $method), $args); } /** * @return Parameter[] */ public function getParameters() { return array_map(function (\ReflectionParameter $parameter) { return new Parameter($parameter); }, $this->method->getParameters()); } /** * @return string|null */ public function getReturnType() { return Reflector::getReturnType($this->method); } } Mockery/Generator/UndefinedTargetClass.php 0000644 00000003206 15107542733 0014662 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; use const PHP_VERSION_ID; class UndefinedTargetClass implements TargetClassInterface { private $name; public function __construct($name) { $this->name = $name; } public static function factory($name) { return new self($name); } public function getAttributes() { return []; } public function getName() { return $this->name; } public function isAbstract() { return false; } public function isFinal() { return false; } public function getMethods() { return array(); } public function getInterfaces() { return array(); } public function getNamespaceName() { $parts = explode("\\", ltrim($this->getName(), "\\")); array_pop($parts); return implode("\\", $parts); } public function inNamespace() { return $this->getNamespaceName() !== ''; } public function getShortName() { $parts = explode("\\", $this->getName()); return array_pop($parts); } public function implementsInterface($interface) { return false; } public function hasInternalAncestor() { return false; } public function __toString() { return $this->name; } } Mockery/Generator/CachingGenerator.php 0000644 00000001551 15107542733 0014030 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; class CachingGenerator implements Generator { protected $generator; protected $cache = array(); public function __construct(Generator $generator) { $this->generator = $generator; } public function generate(MockConfiguration $config) { $hash = $config->getHash(); if (isset($this->cache[$hash])) { return $this->cache[$hash]; } $definition = $this->generator->generate($config); $this->cache[$hash] = $definition; return $definition; } } Mockery/Generator/TargetClassInterface.php 0000644 00000004101 15107542733 0014654 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; interface TargetClassInterface { /** * Returns a new instance of the current * TargetClassInterface's * implementation. * * @param string $name * @return TargetClassInterface */ public static function factory($name); /** * Returns the targetClass's attributes. * * @return array */ public function getAttributes(); /** * Returns the targetClass's name. * * @return string */ public function getName(); /** * Returns the targetClass's methods. * * @return array */ public function getMethods(); /** * Returns the targetClass's interfaces. * * @return array */ public function getInterfaces(); /** * Returns the targetClass's namespace name. * * @return string */ public function getNamespaceName(); /** * Returns the targetClass's short name. * * @return string */ public function getShortName(); /** * Returns whether the targetClass is abstract. * * @return boolean */ public function isAbstract(); /** * Returns whether the targetClass is final. * * @return boolean */ public function isFinal(); /** * Returns whether the targetClass is in namespace. * * @return boolean */ public function inNamespace(); /** * Returns whether the targetClass is in * the passed interface. * * @param mixed $interface * @return boolean */ public function implementsInterface($interface); /** * Returns whether the targetClass has * an internal ancestor. * * @return boolean */ public function hasInternalAncestor(); } Mockery/Generator/MockDefinition.php 0000644 00000001620 15107542733 0013524 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; class MockDefinition { protected $config; protected $code; public function __construct(MockConfiguration $config, $code) { if (!$config->getName()) { throw new \InvalidArgumentException("MockConfiguration must contain a name"); } $this->config = $config; $this->code = $code; } public function getConfig() { return $this->config; } public function getClassName() { return $this->config->getName(); } public function getCode() { return $this->code; } } Mockery/Generator/MockNameBuilder.php 0000644 00000001400 15107542733 0013617 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; class MockNameBuilder { protected static $mockCounter = 0; protected $parts = []; public function addPart($part) { $this->parts[] = $part; return $this; } public function build() { $parts = ['Mockery', static::$mockCounter++]; foreach ($this->parts as $part) { $parts[] = str_replace("\\", "_", $part); } return implode('_', $parts); } } Mockery/Generator/StringManipulationGenerator.php 0000644 00000005406 15107542733 0016326 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; use Mockery\Generator\StringManipulation\Pass\CallTypeHintPass; use Mockery\Generator\StringManipulation\Pass\ClassNamePass; use Mockery\Generator\StringManipulation\Pass\ClassPass; use Mockery\Generator\StringManipulation\Pass\ConstantsPass; use Mockery\Generator\StringManipulation\Pass\InstanceMockPass; use Mockery\Generator\StringManipulation\Pass\InterfacePass; use Mockery\Generator\StringManipulation\Pass\MagicMethodTypeHintsPass; use Mockery\Generator\StringManipulation\Pass\MethodDefinitionPass; use Mockery\Generator\StringManipulation\Pass\Pass; use Mockery\Generator\StringManipulation\Pass\RemoveBuiltinMethodsThatAreFinalPass; use Mockery\Generator\StringManipulation\Pass\RemoveDestructorPass; use Mockery\Generator\StringManipulation\Pass\RemoveUnserializeForInternalSerializableClassesPass; use Mockery\Generator\StringManipulation\Pass\TraitPass; use Mockery\Generator\StringManipulation\Pass\AvoidMethodClashPass; use Mockery\Generator\StringManipulation\Pass\ClassAttributesPass; class StringManipulationGenerator implements Generator { protected $passes = array(); /** * Creates a new StringManipulationGenerator with the default passes * * @return StringManipulationGenerator */ public static function withDefaultPasses() { return new static([ new CallTypeHintPass(), new MagicMethodTypeHintsPass(), new ClassPass(), new TraitPass(), new ClassNamePass(), new InstanceMockPass(), new InterfacePass(), new AvoidMethodClashPass(), new MethodDefinitionPass(), new RemoveUnserializeForInternalSerializableClassesPass(), new RemoveBuiltinMethodsThatAreFinalPass(), new RemoveDestructorPass(), new ConstantsPass(), new ClassAttributesPass(), ]); } public function __construct(array $passes) { $this->passes = $passes; } public function generate(MockConfiguration $config) { $code = file_get_contents(__DIR__ . '/../Mock.php'); $className = $config->getName() ?: $config->generateName(); $namedConfig = $config->rename($className); foreach ($this->passes as $pass) { $code = $pass->apply($code, $namedConfig); } return new MockDefinition($namedConfig, $code); } public function addPass(Pass $pass) { $this->passes[] = $pass; } } Mockery/Generator/Generator.php 0000644 00000000700 15107542733 0012546 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; interface Generator { /** @returns MockDefinition */ public function generate(MockConfiguration $config); } Mockery/Generator/MockConfiguration.php 0000644 00000037444 15107542733 0014260 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; /** * This class describes the configuration of mocks and hides away some of the * reflection implementation */ class MockConfiguration { /** * A class that we'd like to mock */ protected $targetClass; protected $targetClassName; /** * A number of interfaces we'd like to mock, keyed by name to attempt to * keep unique */ protected $targetInterfaces = array(); protected $targetInterfaceNames = array(); /** * A number of traits we'd like to mock, keyed by name to attempt to * keep unique */ protected $targetTraits = array(); protected $targetTraitNames = array(); /** * An object we'd like our mock to proxy to */ protected $targetObject; /** * The class name we'd like to use for a generated mock */ protected $name; /** * Methods that should specifically not be mocked * * This is currently populated with stuff we don't know how to deal with, * should really be somewhere else */ protected $blackListedMethods = array(); /** * If not empty, only these methods will be mocked */ protected $whiteListedMethods = array(); /** * An instance mock is where we override the original class before it's * autoloaded */ protected $instanceMock = false; /** * Param overrides */ protected $parameterOverrides = array(); /** * Instance cache of all methods */ protected $allMethods; /** * If true, overrides original class destructor */ protected $mockOriginalDestructor = false; protected $constantsMap = array(); public function __construct( array $targets = array(), array $blackListedMethods = array(), array $whiteListedMethods = array(), $name = null, $instanceMock = false, array $parameterOverrides = array(), $mockOriginalDestructor = false, array $constantsMap = array() ) { $this->addTargets($targets); $this->blackListedMethods = $blackListedMethods; $this->whiteListedMethods = $whiteListedMethods; $this->name = $name; $this->instanceMock = $instanceMock; $this->parameterOverrides = $parameterOverrides; $this->mockOriginalDestructor = $mockOriginalDestructor; $this->constantsMap = $constantsMap; } /** * Attempt to create a hash of the configuration, in order to allow caching * * @TODO workout if this will work * * @return string */ public function getHash() { $vars = array( 'targetClassName' => $this->targetClassName, 'targetInterfaceNames' => $this->targetInterfaceNames, 'targetTraitNames' => $this->targetTraitNames, 'name' => $this->name, 'blackListedMethods' => $this->blackListedMethods, 'whiteListedMethod' => $this->whiteListedMethods, 'instanceMock' => $this->instanceMock, 'parameterOverrides' => $this->parameterOverrides, 'mockOriginalDestructor' => $this->mockOriginalDestructor ); return md5(serialize($vars)); } /** * Gets a list of methods from the classes, interfaces and objects and * filters them appropriately. Lot's of filtering going on, perhaps we could * have filter classes to iterate through */ public function getMethodsToMock() { $methods = $this->getAllMethods(); foreach ($methods as $key => $method) { if ($method->isFinal()) { unset($methods[$key]); } } /** * Whitelist trumps everything else */ if (count($this->getWhiteListedMethods())) { $whitelist = array_map('strtolower', $this->getWhiteListedMethods()); $methods = array_filter($methods, function ($method) use ($whitelist) { return $method->isAbstract() || in_array(strtolower($method->getName()), $whitelist); }); return $methods; } /** * Remove blacklisted methods */ if (count($this->getBlackListedMethods())) { $blacklist = array_map('strtolower', $this->getBlackListedMethods()); $methods = array_filter($methods, function ($method) use ($blacklist) { return !in_array(strtolower($method->getName()), $blacklist); }); } /** * Internal objects can not be instantiated with newInstanceArgs and if * they implement Serializable, unserialize will have to be called. As * such, we can't mock it and will need a pass to add a dummy * implementation */ if ($this->getTargetClass() && $this->getTargetClass()->implementsInterface("Serializable") && $this->getTargetClass()->hasInternalAncestor()) { $methods = array_filter($methods, function ($method) { return $method->getName() !== "unserialize"; }); } return array_values($methods); } /** * We declare the __call method to handle undefined stuff, if the class * we're mocking has also defined it, we need to comply with their interface */ public function requiresCallTypeHintRemoval() { foreach ($this->getAllMethods() as $method) { if ("__call" === $method->getName()) { $params = $method->getParameters(); return !$params[1]->isArray(); } } return false; } /** * We declare the __callStatic method to handle undefined stuff, if the class * we're mocking has also defined it, we need to comply with their interface */ public function requiresCallStaticTypeHintRemoval() { foreach ($this->getAllMethods() as $method) { if ("__callStatic" === $method->getName()) { $params = $method->getParameters(); return !$params[1]->isArray(); } } return false; } public function rename($className) { $targets = array(); if ($this->targetClassName) { $targets[] = $this->targetClassName; } if ($this->targetInterfaceNames) { $targets = array_merge($targets, $this->targetInterfaceNames); } if ($this->targetTraitNames) { $targets = array_merge($targets, $this->targetTraitNames); } if ($this->targetObject) { $targets[] = $this->targetObject; } return new self( $targets, $this->blackListedMethods, $this->whiteListedMethods, $className, $this->instanceMock, $this->parameterOverrides, $this->mockOriginalDestructor, $this->constantsMap ); } protected function addTarget($target) { if (is_object($target)) { $this->setTargetObject($target); $this->setTargetClassName(get_class($target)); return $this; } if ($target[0] !== "\\") { $target = "\\" . $target; } if (class_exists($target)) { $this->setTargetClassName($target); return $this; } if (interface_exists($target)) { $this->addTargetInterfaceName($target); return $this; } if (trait_exists($target)) { $this->addTargetTraitName($target); return $this; } /** * Default is to set as class, or interface if class already set * * Don't like this condition, can't remember what the default * targetClass is for */ if ($this->getTargetClassName()) { $this->addTargetInterfaceName($target); return $this; } $this->setTargetClassName($target); } protected function addTargets($interfaces) { foreach ($interfaces as $interface) { $this->addTarget($interface); } } public function getTargetClassName() { return $this->targetClassName; } public function getTargetClass() { if ($this->targetClass) { return $this->targetClass; } if (!$this->targetClassName) { return null; } if (class_exists($this->targetClassName)) { $alias = null; if (strpos($this->targetClassName, '@') !== false) { $alias = (new MockNameBuilder()) ->addPart('anonymous_class') ->addPart(md5($this->targetClassName)) ->build(); class_alias($this->targetClassName, $alias); } $dtc = DefinedTargetClass::factory($this->targetClassName, $alias); if ($this->getTargetObject() == false && $dtc->isFinal()) { throw new \Mockery\Exception( 'The class ' . $this->targetClassName . ' is marked final and its methods' . ' cannot be replaced. Classes marked final can be passed in' . ' to \Mockery::mock() as instantiated objects to create a' . ' partial mock, but only if the mock is not subject to type' . ' hinting checks.' ); } $this->targetClass = $dtc; } else { $this->targetClass = UndefinedTargetClass::factory($this->targetClassName); } return $this->targetClass; } public function getTargetTraits() { if (!empty($this->targetTraits)) { return $this->targetTraits; } foreach ($this->targetTraitNames as $targetTrait) { $this->targetTraits[] = DefinedTargetClass::factory($targetTrait); } $this->targetTraits = array_unique($this->targetTraits); // just in case return $this->targetTraits; } public function getTargetInterfaces() { if (!empty($this->targetInterfaces)) { return $this->targetInterfaces; } foreach ($this->targetInterfaceNames as $targetInterface) { if (!interface_exists($targetInterface)) { $this->targetInterfaces[] = UndefinedTargetClass::factory($targetInterface); continue; } $dtc = DefinedTargetClass::factory($targetInterface); $extendedInterfaces = array_keys($dtc->getInterfaces()); $extendedInterfaces[] = $targetInterface; $traversableFound = false; $iteratorShiftedToFront = false; foreach ($extendedInterfaces as $interface) { if (!$traversableFound && preg_match("/^\\?Iterator(|Aggregate)$/i", $interface)) { break; } if (preg_match("/^\\\\?IteratorAggregate$/i", $interface)) { $this->targetInterfaces[] = DefinedTargetClass::factory("\\IteratorAggregate"); $iteratorShiftedToFront = true; } elseif (preg_match("/^\\\\?Iterator$/i", $interface)) { $this->targetInterfaces[] = DefinedTargetClass::factory("\\Iterator"); $iteratorShiftedToFront = true; } elseif (preg_match("/^\\\\?Traversable$/i", $interface)) { $traversableFound = true; } } if ($traversableFound && !$iteratorShiftedToFront) { $this->targetInterfaces[] = DefinedTargetClass::factory("\\IteratorAggregate"); } /** * We never straight up implement Traversable */ if (!preg_match("/^\\\\?Traversable$/i", $targetInterface)) { $this->targetInterfaces[] = $dtc; } } $this->targetInterfaces = array_unique($this->targetInterfaces); // just in case return $this->targetInterfaces; } public function getTargetObject() { return $this->targetObject; } public function getName() { return $this->name; } /** * Generate a suitable name based on the config */ public function generateName() { $nameBuilder = new MockNameBuilder(); if ($this->getTargetObject()) { $className = get_class($this->getTargetObject()); $nameBuilder->addPart(strpos($className, '@') !== false ? md5($className) : $className); } if ($this->getTargetClass()) { $className = $this->getTargetClass()->getName(); $nameBuilder->addPart(strpos($className, '@') !== false ? md5($className) : $className); } foreach ($this->getTargetInterfaces() as $targetInterface) { $nameBuilder->addPart($targetInterface->getName()); } return $nameBuilder->build(); } public function getShortName() { $parts = explode("\\", $this->getName()); return array_pop($parts); } public function getNamespaceName() { $parts = explode("\\", $this->getName()); array_pop($parts); if (count($parts)) { return implode("\\", $parts); } return ""; } public function getBlackListedMethods() { return $this->blackListedMethods; } public function getWhiteListedMethods() { return $this->whiteListedMethods; } public function isInstanceMock() { return $this->instanceMock; } public function getParameterOverrides() { return $this->parameterOverrides; } public function isMockOriginalDestructor() { return $this->mockOriginalDestructor; } protected function setTargetClassName($targetClassName) { $this->targetClassName = $targetClassName; } protected function getAllMethods() { if ($this->allMethods) { return $this->allMethods; } $classes = $this->getTargetInterfaces(); if ($this->getTargetClass()) { $classes[] = $this->getTargetClass(); } $methods = array(); foreach ($classes as $class) { $methods = array_merge($methods, $class->getMethods()); } foreach ($this->getTargetTraits() as $trait) { foreach ($trait->getMethods() as $method) { if ($method->isAbstract()) { $methods[] = $method; } } } $names = array(); $methods = array_filter($methods, function ($method) use (&$names) { if (in_array($method->getName(), $names)) { return false; } $names[] = $method->getName(); return true; }); return $this->allMethods = $methods; } /** * If we attempt to implement Traversable, we must ensure we are also * implementing either Iterator or IteratorAggregate, and that whichever one * it is comes before Traversable in the list of implements. */ protected function addTargetInterfaceName($targetInterface) { $this->targetInterfaceNames[] = $targetInterface; } protected function addTargetTraitName($targetTraitName) { $this->targetTraitNames[] = $targetTraitName; } protected function setTargetObject($object) { $this->targetObject = $object; } public function getConstantsMap() { return $this->constantsMap; } } Mockery/Generator/DefinedTargetClass.php 0000644 00000005427 15107542733 0014326 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; use ReflectionAttribute; use ReflectionClass; use function array_map; use function array_unique; use const PHP_VERSION_ID; class DefinedTargetClass implements TargetClassInterface { private $rfc; private $name; public function __construct(ReflectionClass $rfc, $alias = null) { $this->rfc = $rfc; $this->name = $alias === null ? $rfc->getName() : $alias; } public static function factory($name, $alias = null) { return new self(new ReflectionClass($name), $alias); } public function getAttributes() { if (\PHP_VERSION_ID < 80000) { return []; } return array_unique( array_merge( ['\AllowDynamicProperties'], array_map( static function (ReflectionAttribute $attribute): string { return '\\' . $attribute->getName(); }, $this->rfc->getAttributes() ) ) ); } public function getName() { return $this->name; } public function isAbstract() { return $this->rfc->isAbstract(); } public function isFinal() { return $this->rfc->isFinal(); } public function getMethods() { return array_map(function ($method) { return new Method($method); }, $this->rfc->getMethods()); } public function getInterfaces() { $class = __CLASS__; return array_map(function ($interface) use ($class) { return new $class($interface); }, $this->rfc->getInterfaces()); } public function __toString() { return $this->getName(); } public function getNamespaceName() { return $this->rfc->getNamespaceName(); } public function inNamespace() { return $this->rfc->inNamespace(); } public function getShortName() { return $this->rfc->getShortName(); } public function implementsInterface($interface) { return $this->rfc->implementsInterface($interface); } public function hasInternalAncestor() { if ($this->rfc->isInternal()) { return true; } $child = $this->rfc; while ($parent = $child->getParentClass()) { if ($parent->isInternal()) { return true; } $child = $parent; } return false; } } Mockery/Generator/MockConfigurationBuilder.php 0000644 00000011523 15107542733 0015555 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; class MockConfigurationBuilder { protected $name; protected $blackListedMethods = array( '__call', '__callStatic', '__clone', '__wakeup', '__set', '__get', '__toString', '__isset', '__destruct', '__debugInfo', ## mocking this makes it difficult to debug with xdebug // below are reserved words in PHP "__halt_compiler", "abstract", "and", "array", "as", "break", "callable", "case", "catch", "class", "clone", "const", "continue", "declare", "default", "die", "do", "echo", "else", "elseif", "empty", "enddeclare", "endfor", "endforeach", "endif", "endswitch", "endwhile", "eval", "exit", "extends", "final", "for", "foreach", "function", "global", "goto", "if", "implements", "include", "include_once", "instanceof", "insteadof", "interface", "isset", "list", "namespace", "new", "or", "print", "private", "protected", "public", "require", "require_once", "return", "static", "switch", "throw", "trait", "try", "unset", "use", "var", "while", "xor" ); protected $php7SemiReservedKeywords = [ "callable", "class", "trait", "extends", "implements", "static", "abstract", "final", "public", "protected", "private", "const", "enddeclare", "endfor", "endforeach", "endif", "endwhile", "and", "global", "goto", "instanceof", "insteadof", "interface", "namespace", "new", "or", "xor", "try", "use", "var", "exit", "list", "clone", "include", "include_once", "throw", "array", "print", "echo", "require", "require_once", "return", "else", "elseif", "default", "break", "continue", "switch", "yield", "function", "if", "endswitch", "finally", "for", "foreach", "declare", "case", "do", "while", "as", "catch", "die", "self", "parent", ]; protected $whiteListedMethods = array(); protected $instanceMock = false; protected $parameterOverrides = array(); protected $mockOriginalDestructor = false; protected $targets = array(); protected $constantsMap = array(); public function __construct() { $this->blackListedMethods = array_diff($this->blackListedMethods, $this->php7SemiReservedKeywords); } public function addTarget($target) { $this->targets[] = $target; return $this; } public function addTargets($targets) { foreach ($targets as $target) { $this->addTarget($target); } return $this; } public function setName($name) { $this->name = $name; return $this; } public function addBlackListedMethod($blackListedMethod) { $this->blackListedMethods[] = $blackListedMethod; return $this; } public function addBlackListedMethods(array $blackListedMethods) { foreach ($blackListedMethods as $method) { $this->addBlackListedMethod($method); } return $this; } public function setBlackListedMethods(array $blackListedMethods) { $this->blackListedMethods = $blackListedMethods; return $this; } public function addWhiteListedMethod($whiteListedMethod) { $this->whiteListedMethods[] = $whiteListedMethod; return $this; } public function addWhiteListedMethods(array $whiteListedMethods) { foreach ($whiteListedMethods as $method) { $this->addWhiteListedMethod($method); } return $this; } public function setWhiteListedMethods(array $whiteListedMethods) { $this->whiteListedMethods = $whiteListedMethods; return $this; } public function setInstanceMock($instanceMock) { $this->instanceMock = (bool) $instanceMock; } public function setParameterOverrides(array $overrides) { $this->parameterOverrides = $overrides; } public function setMockOriginalDestructor($mockDestructor) { $this->mockOriginalDestructor = $mockDestructor; return $this; } public function setConstantsMap(array $map) { $this->constantsMap = $map; } public function getMockConfiguration() { return new MockConfiguration( $this->targets, $this->blackListedMethods, $this->whiteListedMethods, $this->name, $this->instanceMock, $this->parameterOverrides, $this->mockOriginalDestructor, $this->constantsMap ); } } Mockery/Generator/Parameter.php 0000644 00000004632 15107542733 0012550 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Generator; use Mockery\Reflector; class Parameter { /** @var int */ private static $parameterCounter = 0; /** @var \ReflectionParameter */ private $rfp; public function __construct(\ReflectionParameter $rfp) { $this->rfp = $rfp; } public function __call($method, array $args) { return call_user_func_array(array($this->rfp, $method), $args); } /** * Get the reflection class for the parameter type, if it exists. * * This will be null if there was no type, or it was a scalar or a union. * * @return \ReflectionClass|null * * @deprecated since 1.3.3 and will be removed in 2.0. */ public function getClass() { $typeHint = Reflector::getTypeHint($this->rfp, true); return \class_exists($typeHint) ? DefinedTargetClass::factory($typeHint, false) : null; } /** * Get the string representation for the paramater type. * * @return string|null */ public function getTypeHint() { return Reflector::getTypeHint($this->rfp); } /** * Get the string representation for the paramater type. * * @return string * * @deprecated since 1.3.2 and will be removed in 2.0. Use getTypeHint() instead. */ public function getTypeHintAsString() { return (string) Reflector::getTypeHint($this->rfp, true); } /** * Get the name of the parameter. * * Some internal classes have funny looking definitions! * * @return string */ public function getName() { $name = $this->rfp->getName(); if (!$name || $name == '...') { $name = 'arg' . self::$parameterCounter++; } return $name; } /** * Determine if the parameter is an array. * * @return bool */ public function isArray() { return Reflector::isArray($this->rfp); } /** * Determine if the parameter is variadic. * * @return bool */ public function isVariadic() { return $this->rfp->isVariadic(); } } Mockery/Matcher/Contains.php 0000644 00000002465 15107542733 0012045 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Contains extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { $values = array_values($actual); foreach ($this->_expected as $exp) { $match = false; foreach ($values as $val) { if ($exp === $val || $exp == $val) { $match = true; break; } } if ($match === false) { return false; } } return true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { $return = '<Contains['; $elements = array(); foreach ($this->_expected as $v) { $elements[] = (string) $v; } $return .= implode(', ', $elements) . ']>'; return $return; } } Mockery/Matcher/Subset.php 0000644 00000004254 15107542733 0011532 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Subset extends MatcherAbstract { private $expected; private $strict = true; /** * @param array $expected Expected subset of data * @param bool $strict Whether to run a strict or loose comparison */ public function __construct(array $expected, $strict = true) { $this->expected = $expected; $this->strict = $strict; } /** * @param array $expected Expected subset of data * * @return Subset */ public static function strict(array $expected) { return new static($expected, true); } /** * @param array $expected Expected subset of data * * @return Subset */ public static function loose(array $expected) { return new static($expected, false); } /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { if (!is_array($actual)) { return false; } if ($this->strict) { return $actual === array_replace_recursive($actual, $this->expected); } return $actual == array_replace_recursive($actual, $this->expected); } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Subset' . $this->formatArray($this->expected) . ">"; } /** * Recursively format an array into the string representation for this matcher * * @param array $array * @return string */ protected function formatArray(array $array) { $elements = []; foreach ($array as $k => $v) { $elements[] = $k . '=' . (is_array($v) ? $this->formatArray($v) : (string) $v); } return "[" . implode(", ", $elements) . "]"; } } Mockery/Matcher/Pattern.php 0000644 00000001433 15107542733 0011676 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Pattern extends MatcherAbstract { /** * Check if the actual value matches the expected pattern. * * @param mixed $actual * @return bool */ public function match(&$actual) { return preg_match($this->_expected, (string) $actual) >= 1; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Pattern>'; } } Mockery/Matcher/AnyArgs.php 0000644 00000001156 15107542733 0011627 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class AnyArgs extends MatcherAbstract implements ArgumentListMatcher { /** * @inheritdoc */ public function match(&$actual) { return true; } /** * @inheritdoc */ public function __toString() { return '<Any Arguments>'; } } Mockery/Matcher/ArgumentListMatcher.php 0000644 00000000554 15107542733 0014206 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; interface ArgumentListMatcher { } Mockery/Matcher/MatcherAbstract.php 0000644 00000002126 15107542733 0013330 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; abstract class MatcherAbstract { /** * The expected value (or part thereof) * * @var mixed */ protected $_expected = null; /** * Set the expected value * * @param mixed $expected */ public function __construct($expected = null) { $this->_expected = $expected; } /** * Check if the actual value matches the expected. * Actual passed by reference to preserve reference trail (where applicable) * back to the original method parameter. * * @param mixed $actual * @return bool */ abstract public function match(&$actual); /** * Return a string representation of this Matcher * * @return string */ abstract public function __toString(); } Mockery/Matcher/Closure.php 0000644 00000001501 15107542733 0011671 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Closure extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { $closure = $this->_expected; $result = $closure($actual); return $result === true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Closure===true>'; } } Mockery/Matcher/MultiArgumentClosure.php 0000644 00000001764 15107542733 0014422 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class MultiArgumentClosure extends MatcherAbstract implements ArgumentListMatcher { /** * Check if the actual value matches the expected. * Actual passed by reference to preserve reference trail (where applicable) * back to the original method parameter. * * @param mixed $actual * @return bool */ public function match(&$actual) { $closure = $this->_expected; return true === call_user_func_array($closure, $actual); } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<MultiArgumentClosure===true>'; } } Mockery/Matcher/AndAnyOtherArgs.php 0000644 00000001361 15107542733 0013252 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class AndAnyOtherArgs extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<AndAnyOthers>'; } } Mockery/Matcher/AnyOf.php 0000644 00000001501 15107542733 0011271 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class AnyOf extends MatcherAbstract { /** * Check if the actual value does not match the expected (in this * case it's specifically NOT expected). * * @param mixed $actual * @return bool */ public function match(&$actual) { return in_array($actual, $this->_expected, true); } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<AnyOf>'; } } Mockery/Matcher/Any.php 0000644 00000001334 15107542733 0011010 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Any extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Any>'; } } Mockery/Matcher/NotAnyOf.php 0000644 00000001671 15107542733 0011762 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class NotAnyOf extends MatcherAbstract { /** * Check if the actual value does not match the expected (in this * case it's specifically NOT expected). * * @param mixed $actual * @return bool */ public function match(&$actual) { foreach ($this->_expected as $exp) { if ($actual === $exp || $actual == $exp) { return false; } } return true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<AnyOf>'; } } Mockery/Matcher/Not.php 0000644 00000001460 15107542733 0011021 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Not extends MatcherAbstract { /** * Check if the actual value does not match the expected (in this * case it's specifically NOT expected). * * @param mixed $actual * @return bool */ public function match(&$actual) { return $actual !== $this->_expected; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Not>'; } } Mockery/Matcher/MustBe.php 0000644 00000001635 15107542733 0011464 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; /** * @deprecated 2.0 Due to ambiguity, use PHPUnit equivalents */ class MustBe extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { if (!is_object($actual)) { return $this->_expected === $actual; } return $this->_expected == $actual; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<MustBe>'; } } Mockery/Matcher/HasValue.php 0000644 00000001503 15107542733 0011767 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class HasValue extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return in_array($this->_expected, $actual); } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { $return = '<HasValue[' . (string) $this->_expected . ']>'; return $return; } } Mockery/Matcher/NoArgs.php 0000644 00000001173 15107542733 0011453 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class NoArgs extends MatcherAbstract implements ArgumentListMatcher { /** * @inheritdoc */ public function match(&$actual) { return count($actual) == 0; } /** * @inheritdoc */ public function __toString() { return '<No Arguments>'; } } Mockery/Matcher/IsEqual.php 0000644 00000001373 15107542733 0011627 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class IsEqual extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return $this->_expected == $actual; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<IsEqual>'; } } Mockery/Matcher/Ducktype.php 0000644 00000001760 15107542733 0012054 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Ducktype extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { if (!is_object($actual)) { return false; } foreach ($this->_expected as $method) { if (!method_exists($actual, $method)) { return false; } } return true; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<Ducktype[' . implode(', ', $this->_expected) . ']>'; } } Mockery/Matcher/HasKey.php 0000644 00000001433 15107542733 0011445 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class HasKey extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return array_key_exists($this->_expected, $actual); } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return "<HasKey[$this->_expected]>"; } } Mockery/Matcher/IsSame.php 0000644 00000001372 15107542733 0011444 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class IsSame extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { return $this->_expected === $actual; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<IsSame>'; } } Mockery/Matcher/Type.php 0000644 00000002271 15107542733 0011203 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\Matcher; class Type extends MatcherAbstract { /** * Check if the actual value matches the expected. * * @param mixed $actual * @return bool */ public function match(&$actual) { if ($this->_expected == 'real') { $function = 'is_float'; } else { $function = 'is_' . strtolower($this->_expected); } if (function_exists($function)) { return $function($actual); } elseif (is_string($this->_expected) && (class_exists($this->_expected) || interface_exists($this->_expected))) { return $actual instanceof $this->_expected; } return false; } /** * Return a string representation of this Matcher * * @return string */ public function __toString() { return '<' . ucfirst($this->_expected) . '>'; } } Mockery/CountValidator/Exception.php 0000644 00000000732 15107542733 0013573 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\CountValidator; use Mockery\Exception\MockeryExceptionInterface; class Exception extends \OutOfBoundsException implements MockeryExceptionInterface { } Mockery/CountValidator/AtLeast.php 0000644 00000002701 15107542733 0013170 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\CountValidator; use Mockery; class AtLeast extends CountValidatorAbstract { /** * Checks if the validator can accept an additional nth call * * @param int $n * @return bool */ public function isEligible($n) { return true; } /** * Validate the call count against this validator * * @param int $n * @return bool */ public function validate($n) { if ($this->_limit > $n) { $exception = new Mockery\Exception\InvalidCountException( 'Method ' . (string) $this->_expectation . ' from ' . $this->_expectation->getMock()->mockery_getName() . ' should be called' . PHP_EOL . ' at least ' . $this->_limit . ' times but called ' . $n . ' times.' ); $exception->setMock($this->_expectation->getMock()) ->setMethodName((string) $this->_expectation) ->setExpectedCountComparative('>=') ->setExpectedCount($this->_limit) ->setActualCount($n); throw $exception; } } } Mockery/CountValidator/Exact.php 0000644 00000002632 15107542733 0012702 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\CountValidator; use Mockery; class Exact extends CountValidatorAbstract { /** * Validate the call count against this validator * * @param int $n * @return bool */ public function validate($n) { if ($this->_limit !== $n) { $because = $this->_expectation->getExceptionMessage(); $exception = new Mockery\Exception\InvalidCountException( 'Method ' . (string) $this->_expectation . ' from ' . $this->_expectation->getMock()->mockery_getName() . ' should be called' . PHP_EOL . ' exactly ' . $this->_limit . ' times but called ' . $n . ' times.' . ($because ? ' Because ' . $this->_expectation->getExceptionMessage() : '') ); $exception->setMock($this->_expectation->getMock()) ->setMethodName((string) $this->_expectation) ->setExpectedCountComparative('=') ->setExpectedCount($this->_limit) ->setActualCount($n); throw $exception; } } } Mockery/CountValidator/AtMost.php 0000644 00000002371 15107542733 0013045 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\CountValidator; use Mockery; class AtMost extends CountValidatorAbstract { /** * Validate the call count against this validator * * @param int $n * @return bool */ public function validate($n) { if ($this->_limit < $n) { $exception = new Mockery\Exception\InvalidCountException( 'Method ' . (string) $this->_expectation . ' from ' . $this->_expectation->getMock()->mockery_getName() . ' should be called' . PHP_EOL . ' at most ' . $this->_limit . ' times but called ' . $n . ' times.' ); $exception->setMock($this->_expectation->getMock()) ->setMethodName((string) $this->_expectation) ->setExpectedCountComparative('<=') ->setExpectedCount($this->_limit) ->setActualCount($n); throw $exception; } } } Mockery/CountValidator/CountValidatorAbstract.php 0000644 00000002434 15107542733 0016260 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery\CountValidator; abstract class CountValidatorAbstract { /** * Expectation for which this validator is assigned * * @var \Mockery\Expectation */ protected $_expectation = null; /** * Call count limit * * @var int */ protected $_limit = null; /** * Set Expectation object and upper call limit * * @param \Mockery\Expectation $expectation * @param int $limit */ public function __construct(\Mockery\Expectation $expectation, $limit) { $this->_expectation = $expectation; $this->_limit = $limit; } /** * Checks if the validator can accept an additional nth call * * @param int $n * @return bool */ public function isEligible($n) { return ($n < $this->_limit); } /** * Validate the call count against this validator * * @param int $n * @return bool */ abstract public function validate($n); } Mockery/VerificationExpectation.php 0000644 00000001101 15107542733 0013514 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class VerificationExpectation extends Expectation { public function clearCountValidators() { $this->_countValidators = array(); } public function __clone() { parent::__clone(); $this->_actualCount = 0; } } Mockery/ExpectationDirector.php 0000644 00000012673 15107542733 0012665 0 ustar 00 <?php /** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */ namespace Mockery; class ExpectationDirector { /** * Method name the director is directing * * @var string */ protected $_name = null; /** * Mock object the director is attached to * * @var \Mockery\MockInterface|\Mockery\LegacyMockInterface */ protected $_mock = null; /** * Stores an array of all expectations for this mock * * @var array */ protected $_expectations = array(); /** * The expected order of next call * * @var int */ protected $_expectedOrder = null; /** * Stores an array of all default expectations for this mock * * @var array */ protected $_defaults = array(); /** * Constructor * * @param string $name * @param \Mockery\LegacyMockInterface $mock */ public function __construct($name, \Mockery\LegacyMockInterface $mock) { $this->_name = $name; $this->_mock = $mock; } /** * Add a new expectation to the director * * @param \Mockery\Expectation $expectation */ public function addExpectation(\Mockery\Expectation $expectation) { $this->_expectations[] = $expectation; } /** * Handle a method call being directed by this instance * * @param array $args * @return mixed */ public function call(array $args) { $expectation = $this->findExpectation($args); if (is_null($expectation)) { $exception = new \Mockery\Exception\NoMatchingExpectationException( 'No matching handler found for ' . $this->_mock->mockery_getName() . '::' . \Mockery::formatArgs($this->_name, $args) . '. Either the method was unexpected or its arguments matched' . ' no expected argument list for this method' . PHP_EOL . PHP_EOL . \Mockery::formatObjects($args) ); $exception->setMock($this->_mock) ->setMethodName($this->_name) ->setActualArguments($args); throw $exception; } return $expectation->verifyCall($args); } /** * Verify all expectations of the director * * @throws \Mockery\CountValidator\Exception * @return void */ public function verify() { if (!empty($this->_expectations)) { foreach ($this->_expectations as $exp) { $exp->verify(); } } else { foreach ($this->_defaults as $exp) { $exp->verify(); } } } /** * Attempt to locate an expectation matching the provided args * * @param array $args * @return mixed */ public function findExpectation(array $args) { $expectation = null; if (!empty($this->_expectations)) { $expectation = $this->_findExpectationIn($this->_expectations, $args); } if ($expectation === null && !empty($this->_defaults)) { $expectation = $this->_findExpectationIn($this->_defaults, $args); } return $expectation; } /** * Make the given expectation a default for all others assuming it was * correctly created last * * @param \Mockery\Expectation $expectation */ public function makeExpectationDefault(\Mockery\Expectation $expectation) { $last = end($this->_expectations); if ($last === $expectation) { array_pop($this->_expectations); array_unshift($this->_defaults, $expectation); } else { throw new \Mockery\Exception( 'Cannot turn a previously defined expectation into a default' ); } } /** * Search current array of expectations for a match * * @param array $expectations * @param array $args * @return mixed */ protected function _findExpectationIn(array $expectations, array $args) { foreach ($expectations as $exp) { if ($exp->isEligible() && $exp->matchArgs($args)) { return $exp; } } foreach ($expectations as $exp) { if ($exp->matchArgs($args)) { return $exp; } } } /** * Return all expectations assigned to this director * * @return array */ public function getExpectations() { return $this->_expectations; } /** * Return all expectations assigned to this director * * @return array */ public function getDefaultExpectations() { return $this->_defaults; } /** * Return the number of expectations assigned to this director. * * @return int */ public function getExpectationCount() { $count = 0; /** @var Expectation $expectations */ $expectations = $this->getExpectations() ?: $this->getDefaultExpectations(); foreach ($expectations as $expectation) { if ($expectation->isCallCountConstrained()) { $count++; } } return $count; } } HTMLPurifier.autoload.php 0000644 00000001616 15111210765 0011341 0 ustar 00 <?php /** * @file * Convenience file that registers autoload handler for HTML Purifier. * It also does some sanity checks. */ if (function_exists('spl_autoload_register') && function_exists('spl_autoload_unregister')) { // We need unregister for our pre-registering functionality HTMLPurifier_Bootstrap::registerAutoload(); if (function_exists('__autoload')) { // Be polite and ensure that userland autoload gets retained spl_autoload_register('__autoload'); } } elseif (!function_exists('__autoload')) { require dirname(__FILE__) . '/HTMLPurifier.autoload-legacy.php'; } // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.zend_ze1_compatibility_modeRemoved if (ini_get('zend.ze1_compatibility_mode')) { trigger_error("HTML Purifier is not compatible with zend.ze1_compatibility_mode; please turn it off", E_USER_ERROR); } // vim: et sw=4 sts=4 HTMLPurifier.auto.php 0000644 00000000422 15111210765 0010473 0 ustar 00 <?php /** * This is a stub include that automatically configures the include path. */ set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() ); require_once 'HTMLPurifier/Bootstrap.php'; require_once 'HTMLPurifier.autoload.php'; // vim: et sw=4 sts=4 HTMLPurifier.path.php 0000644 00000000353 15111210765 0010462 0 ustar 00 <?php /** * @file * Convenience stub file that adds HTML Purifier's library file to the path * without any other side-effects. */ set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() ); // vim: et sw=4 sts=4 HTMLPurifier.php 0000644 00000023713 15111210765 0007534 0 ustar 00 <?php /*! @mainpage * * HTML Purifier is an HTML filter that will take an arbitrary snippet of * HTML and rigorously test, validate and filter it into a version that * is safe for output onto webpages. It achieves this by: * * -# Lexing (parsing into tokens) the document, * -# Executing various strategies on the tokens: * -# Removing all elements not in the whitelist, * -# Making the tokens well-formed, * -# Fixing the nesting of the nodes, and * -# Validating attributes of the nodes; and * -# Generating HTML from the purified tokens. * * However, most users will only need to interface with the HTMLPurifier * and HTMLPurifier_Config. */ /* HTML Purifier 4.15.0 - Standards Compliant HTML Filtering Copyright (C) 2006-2008 Edward Z. Yang This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * Facade that coordinates HTML Purifier's subsystems in order to purify HTML. * * @note There are several points in which configuration can be specified * for HTML Purifier. The precedence of these (from lowest to * highest) is as follows: * -# Instance: new HTMLPurifier($config) * -# Invocation: purify($html, $config) * These configurations are entirely independent of each other and * are *not* merged (this behavior may change in the future). * * @todo We need an easier way to inject strategies using the configuration * object. */ class HTMLPurifier { /** * Version of HTML Purifier. * @type string */ public $version = '4.15.0'; /** * Constant with version of HTML Purifier. */ const VERSION = '4.15.0'; /** * Global configuration object. * @type HTMLPurifier_Config */ public $config; /** * Array of extra filter objects to run on HTML, * for backwards compatibility. * @type HTMLPurifier_Filter[] */ private $filters = array(); /** * Single instance of HTML Purifier. * @type HTMLPurifier */ private static $instance; /** * @type HTMLPurifier_Strategy_Core */ protected $strategy; /** * @type HTMLPurifier_Generator */ protected $generator; /** * Resultant context of last run purification. * Is an array of contexts if the last called method was purifyArray(). * @type HTMLPurifier_Context */ public $context; /** * Initializes the purifier. * * @param HTMLPurifier_Config|mixed $config Optional HTMLPurifier_Config object * for all instances of the purifier, if omitted, a default * configuration is supplied (which can be overridden on a * per-use basis). * The parameter can also be any type that * HTMLPurifier_Config::create() supports. */ public function __construct($config = null) { $this->config = HTMLPurifier_Config::create($config); $this->strategy = new HTMLPurifier_Strategy_Core(); } /** * Adds a filter to process the output. First come first serve * * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object */ public function addFilter($filter) { trigger_error( 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . ' in the Filter namespace or Filter.Custom', E_USER_WARNING ); $this->filters[] = $filter; } /** * Filters an HTML snippet/document to be XSS-free and standards-compliant. * * @param string $html String of HTML to purify * @param HTMLPurifier_Config $config Config object for this operation, * if omitted, defaults to the config object specified during this * object's construction. The parameter can also be any type * that HTMLPurifier_Config::create() supports. * * @return string Purified HTML */ public function purify($html, $config = null) { // :TODO: make the config merge in, instead of replace $config = $config ? HTMLPurifier_Config::create($config) : $this->config; // implementation is partially environment dependant, partially // configuration dependant $lexer = HTMLPurifier_Lexer::create($config); $context = new HTMLPurifier_Context(); // setup HTML generator $this->generator = new HTMLPurifier_Generator($config, $context); $context->register('Generator', $this->generator); // set up global context variables if ($config->get('Core.CollectErrors')) { // may get moved out if other facilities use it $language_factory = HTMLPurifier_LanguageFactory::instance(); $language = $language_factory->create($config, $context); $context->register('Locale', $language); $error_collector = new HTMLPurifier_ErrorCollector($context); $context->register('ErrorCollector', $error_collector); } // setup id_accumulator context, necessary due to the fact that // AttrValidator can be called from many places $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); $context->register('IDAccumulator', $id_accumulator); $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); // setup filters $filter_flags = $config->getBatch('Filter'); $custom_filters = $filter_flags['Custom']; unset($filter_flags['Custom']); $filters = array(); foreach ($filter_flags as $filter => $flag) { if (!$flag) { continue; } if (strpos($filter, '.') !== false) { continue; } $class = "HTMLPurifier_Filter_$filter"; $filters[] = new $class; } foreach ($custom_filters as $filter) { // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat $filters[] = $filter; } $filters = array_merge($filters, $this->filters); // maybe prepare(), but later for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { $html = $filters[$i]->preFilter($html, $config, $context); } // purified HTML $html = $this->generator->generateFromTokens( // list of tokens $this->strategy->execute( // list of un-purified tokens $lexer->tokenizeHTML( // un-purified HTML $html, $config, $context ), $config, $context ) ); for ($i = $filter_size - 1; $i >= 0; $i--) { $html = $filters[$i]->postFilter($html, $config, $context); } $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); $this->context =& $context; return $html; } /** * Filters an array of HTML snippets * * @param string[] $array_of_html Array of html snippets * @param HTMLPurifier_Config $config Optional config object for this operation. * See HTMLPurifier::purify() for more details. * * @return string[] Array of purified HTML */ public function purifyArray($array_of_html, $config = null) { $context_array = array(); $array = array(); foreach($array_of_html as $key=>$value){ if (is_array($value)) { $array[$key] = $this->purifyArray($value, $config); } else { $array[$key] = $this->purify($value, $config); } $context_array[$key] = $this->context; } $this->context = $context_array; return $array; } /** * Singleton for enforcing just one HTML Purifier in your system * * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype * HTMLPurifier instance to overload singleton with, * or HTMLPurifier_Config instance to configure the * generated version with. * * @return HTMLPurifier */ public static function instance($prototype = null) { if (!self::$instance || $prototype) { if ($prototype instanceof HTMLPurifier) { self::$instance = $prototype; } elseif ($prototype) { self::$instance = new HTMLPurifier($prototype); } else { self::$instance = new HTMLPurifier(); } } return self::$instance; } /** * Singleton for enforcing just one HTML Purifier in your system * * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype * HTMLPurifier instance to overload singleton with, * or HTMLPurifier_Config instance to configure the * generated version with. * * @return HTMLPurifier * @note Backwards compatibility, see instance() */ public static function getInstance($prototype = null) { return HTMLPurifier::instance($prototype); } } // vim: et sw=4 sts=4 HTMLPurifier.composer.php 0000644 00000000145 15111210765 0011354 0 ustar 00 <?php if (!defined('HTMLPURIFIER_PREFIX')) { define('HTMLPURIFIER_PREFIX', dirname(__FILE__)); } HTMLPurifier.func.php 0000644 00000001100 15111210765 0010450 0 ustar 00 <?php /** * @file * Defines a function wrapper for HTML Purifier for quick use. * @note ''HTMLPurifier()'' is NOT the same as ''new HTMLPurifier()'' */ /** * Purify HTML. * @param string $html String HTML to purify * @param mixed $config Configuration to use, can be any value accepted by * HTMLPurifier_Config::create() * @return string */ function HTMLPurifier($html, $config = null) { static $purifier = false; if (!$purifier) { $purifier = new HTMLPurifier(); } return $purifier->purify($html, $config); } // vim: et sw=4 sts=4 HTMLPurifier.includes.php 0000644 00000024515 15111210765 0011342 0 ustar 00 <?php /** * @file * This file was auto-generated by generate-includes.php and includes all of * the core files required by HTML Purifier. Use this if performance is a * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS * FILE, changes will be overwritten the next time the script is run. * * @version 4.15.0 * * @warning * You must *not* include any other HTML Purifier files before this file, * because 'require' not 'require_once' is used. * * @warning * This file requires that the include path contains the HTML Purifier * library directory; this is not auto-set. */ require 'HTMLPurifier.php'; require 'HTMLPurifier/Arborize.php'; require 'HTMLPurifier/AttrCollections.php'; require 'HTMLPurifier/AttrDef.php'; require 'HTMLPurifier/AttrTransform.php'; require 'HTMLPurifier/AttrTypes.php'; require 'HTMLPurifier/AttrValidator.php'; require 'HTMLPurifier/Bootstrap.php'; require 'HTMLPurifier/Definition.php'; require 'HTMLPurifier/CSSDefinition.php'; require 'HTMLPurifier/ChildDef.php'; require 'HTMLPurifier/Config.php'; require 'HTMLPurifier/ConfigSchema.php'; require 'HTMLPurifier/ContentSets.php'; require 'HTMLPurifier/Context.php'; require 'HTMLPurifier/DefinitionCache.php'; require 'HTMLPurifier/DefinitionCacheFactory.php'; require 'HTMLPurifier/Doctype.php'; require 'HTMLPurifier/DoctypeRegistry.php'; require 'HTMLPurifier/ElementDef.php'; require 'HTMLPurifier/Encoder.php'; require 'HTMLPurifier/EntityLookup.php'; require 'HTMLPurifier/EntityParser.php'; require 'HTMLPurifier/ErrorCollector.php'; require 'HTMLPurifier/ErrorStruct.php'; require 'HTMLPurifier/Exception.php'; require 'HTMLPurifier/Filter.php'; require 'HTMLPurifier/Generator.php'; require 'HTMLPurifier/HTMLDefinition.php'; require 'HTMLPurifier/HTMLModule.php'; require 'HTMLPurifier/HTMLModuleManager.php'; require 'HTMLPurifier/IDAccumulator.php'; require 'HTMLPurifier/Injector.php'; require 'HTMLPurifier/Language.php'; require 'HTMLPurifier/LanguageFactory.php'; require 'HTMLPurifier/Length.php'; require 'HTMLPurifier/Lexer.php'; require 'HTMLPurifier/Node.php'; require 'HTMLPurifier/PercentEncoder.php'; require 'HTMLPurifier/PropertyList.php'; require 'HTMLPurifier/PropertyListIterator.php'; require 'HTMLPurifier/Queue.php'; require 'HTMLPurifier/Strategy.php'; require 'HTMLPurifier/StringHash.php'; require 'HTMLPurifier/StringHashParser.php'; require 'HTMLPurifier/TagTransform.php'; require 'HTMLPurifier/Token.php'; require 'HTMLPurifier/TokenFactory.php'; require 'HTMLPurifier/URI.php'; require 'HTMLPurifier/URIDefinition.php'; require 'HTMLPurifier/URIFilter.php'; require 'HTMLPurifier/URIParser.php'; require 'HTMLPurifier/URIScheme.php'; require 'HTMLPurifier/URISchemeRegistry.php'; require 'HTMLPurifier/UnitConverter.php'; require 'HTMLPurifier/VarParser.php'; require 'HTMLPurifier/VarParserException.php'; require 'HTMLPurifier/Zipper.php'; require 'HTMLPurifier/AttrDef/CSS.php'; require 'HTMLPurifier/AttrDef/Clone.php'; require 'HTMLPurifier/AttrDef/Enum.php'; require 'HTMLPurifier/AttrDef/Integer.php'; require 'HTMLPurifier/AttrDef/Lang.php'; require 'HTMLPurifier/AttrDef/Switch.php'; require 'HTMLPurifier/AttrDef/Text.php'; require 'HTMLPurifier/AttrDef/URI.php'; require 'HTMLPurifier/AttrDef/CSS/Number.php'; require 'HTMLPurifier/AttrDef/CSS/AlphaValue.php'; require 'HTMLPurifier/AttrDef/CSS/Background.php'; require 'HTMLPurifier/AttrDef/CSS/BackgroundPosition.php'; require 'HTMLPurifier/AttrDef/CSS/Border.php'; require 'HTMLPurifier/AttrDef/CSS/Color.php'; require 'HTMLPurifier/AttrDef/CSS/Composite.php'; require 'HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php'; require 'HTMLPurifier/AttrDef/CSS/Filter.php'; require 'HTMLPurifier/AttrDef/CSS/Font.php'; require 'HTMLPurifier/AttrDef/CSS/FontFamily.php'; require 'HTMLPurifier/AttrDef/CSS/Ident.php'; require 'HTMLPurifier/AttrDef/CSS/ImportantDecorator.php'; require 'HTMLPurifier/AttrDef/CSS/Length.php'; require 'HTMLPurifier/AttrDef/CSS/ListStyle.php'; require 'HTMLPurifier/AttrDef/CSS/Multiple.php'; require 'HTMLPurifier/AttrDef/CSS/Percentage.php'; require 'HTMLPurifier/AttrDef/CSS/TextDecoration.php'; require 'HTMLPurifier/AttrDef/CSS/URI.php'; require 'HTMLPurifier/AttrDef/HTML/Bool.php'; require 'HTMLPurifier/AttrDef/HTML/Nmtokens.php'; require 'HTMLPurifier/AttrDef/HTML/Class.php'; require 'HTMLPurifier/AttrDef/HTML/Color.php'; require 'HTMLPurifier/AttrDef/HTML/ContentEditable.php'; require 'HTMLPurifier/AttrDef/HTML/FrameTarget.php'; require 'HTMLPurifier/AttrDef/HTML/ID.php'; require 'HTMLPurifier/AttrDef/HTML/Pixels.php'; require 'HTMLPurifier/AttrDef/HTML/Length.php'; require 'HTMLPurifier/AttrDef/HTML/LinkTypes.php'; require 'HTMLPurifier/AttrDef/HTML/MultiLength.php'; require 'HTMLPurifier/AttrDef/URI/Email.php'; require 'HTMLPurifier/AttrDef/URI/Host.php'; require 'HTMLPurifier/AttrDef/URI/IPv4.php'; require 'HTMLPurifier/AttrDef/URI/IPv6.php'; require 'HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php'; require 'HTMLPurifier/AttrTransform/Background.php'; require 'HTMLPurifier/AttrTransform/BdoDir.php'; require 'HTMLPurifier/AttrTransform/BgColor.php'; require 'HTMLPurifier/AttrTransform/BoolToCSS.php'; require 'HTMLPurifier/AttrTransform/Border.php'; require 'HTMLPurifier/AttrTransform/EnumToCSS.php'; require 'HTMLPurifier/AttrTransform/ImgRequired.php'; require 'HTMLPurifier/AttrTransform/ImgSpace.php'; require 'HTMLPurifier/AttrTransform/Input.php'; require 'HTMLPurifier/AttrTransform/Lang.php'; require 'HTMLPurifier/AttrTransform/Length.php'; require 'HTMLPurifier/AttrTransform/Name.php'; require 'HTMLPurifier/AttrTransform/NameSync.php'; require 'HTMLPurifier/AttrTransform/Nofollow.php'; require 'HTMLPurifier/AttrTransform/SafeEmbed.php'; require 'HTMLPurifier/AttrTransform/SafeObject.php'; require 'HTMLPurifier/AttrTransform/SafeParam.php'; require 'HTMLPurifier/AttrTransform/ScriptRequired.php'; require 'HTMLPurifier/AttrTransform/TargetBlank.php'; require 'HTMLPurifier/AttrTransform/TargetNoopener.php'; require 'HTMLPurifier/AttrTransform/TargetNoreferrer.php'; require 'HTMLPurifier/AttrTransform/Textarea.php'; require 'HTMLPurifier/ChildDef/Chameleon.php'; require 'HTMLPurifier/ChildDef/Custom.php'; require 'HTMLPurifier/ChildDef/Empty.php'; require 'HTMLPurifier/ChildDef/List.php'; require 'HTMLPurifier/ChildDef/Required.php'; require 'HTMLPurifier/ChildDef/Optional.php'; require 'HTMLPurifier/ChildDef/StrictBlockquote.php'; require 'HTMLPurifier/ChildDef/Table.php'; require 'HTMLPurifier/DefinitionCache/Decorator.php'; require 'HTMLPurifier/DefinitionCache/Null.php'; require 'HTMLPurifier/DefinitionCache/Serializer.php'; require 'HTMLPurifier/DefinitionCache/Decorator/Cleanup.php'; require 'HTMLPurifier/DefinitionCache/Decorator/Memory.php'; require 'HTMLPurifier/HTMLModule/Bdo.php'; require 'HTMLPurifier/HTMLModule/CommonAttributes.php'; require 'HTMLPurifier/HTMLModule/Edit.php'; require 'HTMLPurifier/HTMLModule/Forms.php'; require 'HTMLPurifier/HTMLModule/Hypertext.php'; require 'HTMLPurifier/HTMLModule/Iframe.php'; require 'HTMLPurifier/HTMLModule/Image.php'; require 'HTMLPurifier/HTMLModule/Legacy.php'; require 'HTMLPurifier/HTMLModule/List.php'; require 'HTMLPurifier/HTMLModule/Name.php'; require 'HTMLPurifier/HTMLModule/Nofollow.php'; require 'HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php'; require 'HTMLPurifier/HTMLModule/Object.php'; require 'HTMLPurifier/HTMLModule/Presentation.php'; require 'HTMLPurifier/HTMLModule/Proprietary.php'; require 'HTMLPurifier/HTMLModule/Ruby.php'; require 'HTMLPurifier/HTMLModule/SafeEmbed.php'; require 'HTMLPurifier/HTMLModule/SafeObject.php'; require 'HTMLPurifier/HTMLModule/SafeScripting.php'; require 'HTMLPurifier/HTMLModule/Scripting.php'; require 'HTMLPurifier/HTMLModule/StyleAttribute.php'; require 'HTMLPurifier/HTMLModule/Tables.php'; require 'HTMLPurifier/HTMLModule/Target.php'; require 'HTMLPurifier/HTMLModule/TargetBlank.php'; require 'HTMLPurifier/HTMLModule/TargetNoopener.php'; require 'HTMLPurifier/HTMLModule/TargetNoreferrer.php'; require 'HTMLPurifier/HTMLModule/Text.php'; require 'HTMLPurifier/HTMLModule/Tidy.php'; require 'HTMLPurifier/HTMLModule/XMLCommonAttributes.php'; require 'HTMLPurifier/HTMLModule/Tidy/Name.php'; require 'HTMLPurifier/HTMLModule/Tidy/Proprietary.php'; require 'HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php'; require 'HTMLPurifier/HTMLModule/Tidy/Strict.php'; require 'HTMLPurifier/HTMLModule/Tidy/Transitional.php'; require 'HTMLPurifier/HTMLModule/Tidy/XHTML.php'; require 'HTMLPurifier/Injector/AutoParagraph.php'; require 'HTMLPurifier/Injector/DisplayLinkURI.php'; require 'HTMLPurifier/Injector/Linkify.php'; require 'HTMLPurifier/Injector/PurifierLinkify.php'; require 'HTMLPurifier/Injector/RemoveEmpty.php'; require 'HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php'; require 'HTMLPurifier/Injector/SafeObject.php'; require 'HTMLPurifier/Lexer/DOMLex.php'; require 'HTMLPurifier/Lexer/DirectLex.php'; require 'HTMLPurifier/Node/Comment.php'; require 'HTMLPurifier/Node/Element.php'; require 'HTMLPurifier/Node/Text.php'; require 'HTMLPurifier/Strategy/Composite.php'; require 'HTMLPurifier/Strategy/Core.php'; require 'HTMLPurifier/Strategy/FixNesting.php'; require 'HTMLPurifier/Strategy/MakeWellFormed.php'; require 'HTMLPurifier/Strategy/RemoveForeignElements.php'; require 'HTMLPurifier/Strategy/ValidateAttributes.php'; require 'HTMLPurifier/TagTransform/Font.php'; require 'HTMLPurifier/TagTransform/Simple.php'; require 'HTMLPurifier/Token/Comment.php'; require 'HTMLPurifier/Token/Tag.php'; require 'HTMLPurifier/Token/Empty.php'; require 'HTMLPurifier/Token/End.php'; require 'HTMLPurifier/Token/Start.php'; require 'HTMLPurifier/Token/Text.php'; require 'HTMLPurifier/URIFilter/DisableExternal.php'; require 'HTMLPurifier/URIFilter/DisableExternalResources.php'; require 'HTMLPurifier/URIFilter/DisableResources.php'; require 'HTMLPurifier/URIFilter/HostBlacklist.php'; require 'HTMLPurifier/URIFilter/MakeAbsolute.php'; require 'HTMLPurifier/URIFilter/Munge.php'; require 'HTMLPurifier/URIFilter/SafeIframe.php'; require 'HTMLPurifier/URIScheme/data.php'; require 'HTMLPurifier/URIScheme/file.php'; require 'HTMLPurifier/URIScheme/ftp.php'; require 'HTMLPurifier/URIScheme/http.php'; require 'HTMLPurifier/URIScheme/https.php'; require 'HTMLPurifier/URIScheme/mailto.php'; require 'HTMLPurifier/URIScheme/news.php'; require 'HTMLPurifier/URIScheme/nntp.php'; require 'HTMLPurifier/URIScheme/tel.php'; require 'HTMLPurifier/VarParser/Flexible.php'; require 'HTMLPurifier/VarParser/Native.php'; HTMLPurifier/UnitConverter.php 0000644 00000023622 15111210765 0012342 0 ustar 00 <?php /** * Class for converting between different unit-lengths as specified by * CSS. */ class HTMLPurifier_UnitConverter { const ENGLISH = 1; const METRIC = 2; const DIGITAL = 3; /** * Units information array. Units are grouped into measuring systems * (English, Metric), and are assigned an integer representing * the conversion factor between that unit and the smallest unit in * the system. Numeric indexes are actually magical constants that * encode conversion data from one system to the next, with a O(n^2) * constraint on memory (this is generally not a problem, since * the number of measuring systems is small.) */ protected static $units = array( self::ENGLISH => array( 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary 'pt' => 4, 'pc' => 48, 'in' => 288, self::METRIC => array('pt', '0.352777778', 'mm'), ), self::METRIC => array( 'mm' => 1, 'cm' => 10, self::ENGLISH => array('mm', '2.83464567', 'pt'), ), ); /** * Minimum bcmath precision for output. * @type int */ protected $outputPrecision; /** * Bcmath precision for internal calculations. * @type int */ protected $internalPrecision; /** * Whether or not BCMath is available. * @type bool */ private $bcmath; public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) { $this->outputPrecision = $output_precision; $this->internalPrecision = $internal_precision; $this->bcmath = !$force_no_bcmath && function_exists('bcmul'); } /** * Converts a length object of one unit into another unit. * @param HTMLPurifier_Length $length * Instance of HTMLPurifier_Length to convert. You must validate() * it before passing it here! * @param string $to_unit * Unit to convert to. * @return HTMLPurifier_Length|bool * @note * About precision: This conversion function pays very special * attention to the incoming precision of values and attempts * to maintain a number of significant figure. Results are * fairly accurate up to nine digits. Some caveats: * - If a number is zero-padded as a result of this significant * figure tracking, the zeroes will be eliminated. * - If a number contains less than four sigfigs ($outputPrecision) * and this causes some decimals to be excluded, those * decimals will be added on. */ public function convert($length, $to_unit) { if (!$length->isValid()) { return false; } $n = $length->getN(); $unit = $length->getUnit(); if ($n === '0' || $unit === false) { return new HTMLPurifier_Length('0', false); } $state = $dest_state = false; foreach (self::$units as $k => $x) { if (isset($x[$unit])) { $state = $k; } if (isset($x[$to_unit])) { $dest_state = $k; } } if (!$state || !$dest_state) { return false; } // Some calculations about the initial precision of the number; // this will be useful when we need to do final rounding. $sigfigs = $this->getSigFigs($n); if ($sigfigs < $this->outputPrecision) { $sigfigs = $this->outputPrecision; } // BCMath's internal precision deals only with decimals. Use // our default if the initial number has no decimals, or increase // it by how ever many decimals, thus, the number of guard digits // will always be greater than or equal to internalPrecision. $log = (int)floor(log(abs($n), 10)); $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision for ($i = 0; $i < 2; $i++) { // Determine what unit IN THIS SYSTEM we need to convert to if ($dest_state === $state) { // Simple conversion $dest_unit = $to_unit; } else { // Convert to the smallest unit, pending a system shift $dest_unit = self::$units[$state][$dest_state][0]; } // Do the conversion if necessary if ($dest_unit !== $unit) { $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp); $n = $this->mul($n, $factor, $cp); $unit = $dest_unit; } // Output was zero, so bail out early. Shouldn't ever happen. if ($n === '') { $n = '0'; $unit = $to_unit; break; } // It was a simple conversion, so bail out if ($dest_state === $state) { break; } if ($i !== 0) { // Conversion failed! Apparently, the system we forwarded // to didn't have this unit. This should never happen! return false; } // Pre-condition: $i == 0 // Perform conversion to next system of units $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp); $unit = self::$units[$state][$dest_state][2]; $state = $dest_state; // One more loop around to convert the unit in the new system. } // Post-condition: $unit == $to_unit if ($unit !== $to_unit) { return false; } // Useful for debugging: //echo "<pre>n"; //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n"; $n = $this->round($n, $sigfigs); if (strpos($n, '.') !== false) { $n = rtrim($n, '0'); } $n = rtrim($n, '.'); return new HTMLPurifier_Length($n, $unit); } /** * Returns the number of significant figures in a string number. * @param string $n Decimal number * @return int number of sigfigs */ public function getSigFigs($n) { $n = ltrim($n, '0+-'); $dp = strpos($n, '.'); // decimal position if ($dp === false) { $sigfigs = strlen(rtrim($n, '0')); } else { $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character if ($dp !== 0) { $sigfigs--; } } return $sigfigs; } /** * Adds two numbers, using arbitrary precision when available. * @param string $s1 * @param string $s2 * @param int $scale * @return string */ private function add($s1, $s2, $scale) { if ($this->bcmath) { return bcadd($s1, $s2, $scale); } else { return $this->scale((float)$s1 + (float)$s2, $scale); } } /** * Multiples two numbers, using arbitrary precision when available. * @param string $s1 * @param string $s2 * @param int $scale * @return string */ private function mul($s1, $s2, $scale) { if ($this->bcmath) { return bcmul($s1, $s2, $scale); } else { return $this->scale((float)$s1 * (float)$s2, $scale); } } /** * Divides two numbers, using arbitrary precision when available. * @param string $s1 * @param string $s2 * @param int $scale * @return string */ private function div($s1, $s2, $scale) { if ($this->bcmath) { return bcdiv($s1, $s2, $scale); } else { return $this->scale((float)$s1 / (float)$s2, $scale); } } /** * Rounds a number according to the number of sigfigs it should have, * using arbitrary precision when available. * @param float $n * @param int $sigfigs * @return string */ private function round($n, $sigfigs) { $new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1 $rp = $sigfigs - $new_log - 1; // Number of decimal places needed $neg = $n < 0 ? '-' : ''; // Negative sign if ($this->bcmath) { if ($rp >= 0) { $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1); $n = bcdiv($n, '1', $rp); } else { // This algorithm partially depends on the standardized // form of numbers that comes out of bcmath. $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0); $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1); } return $n; } else { return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1); } } /** * Scales a float to $scale digits right of decimal point, like BCMath. * @param float $r * @param int $scale * @return string */ private function scale($r, $scale) { if ($scale < 0) { // The f sprintf type doesn't support negative numbers, so we // need to cludge things manually. First get the string. $r = sprintf('%.0f', (float)$r); // Due to floating point precision loss, $r will more than likely // look something like 4652999999999.9234. We grab one more digit // than we need to precise from $r and then use that to round // appropriately. $precise = (string)round(substr($r, 0, strlen($r) + $scale), -1); // Now we return it, truncating the zero that was rounded off. return substr($precise, 0, -1) . str_repeat('0', -$scale + 1); } return sprintf('%.' . $scale . 'f', (float)$r); } } // vim: et sw=4 sts=4 HTMLPurifier/VarParser.php 0000644 00000013546 15111210766 0011445 0 ustar 00 <?php /** * Parses string representations into their corresponding native PHP * variable type. The base implementation does a simple type-check. */ class HTMLPurifier_VarParser { const C_STRING = 1; const ISTRING = 2; const TEXT = 3; const ITEXT = 4; const C_INT = 5; const C_FLOAT = 6; const C_BOOL = 7; const LOOKUP = 8; const ALIST = 9; const HASH = 10; const C_MIXED = 11; /** * Lookup table of allowed types. Mainly for backwards compatibility, but * also convenient for transforming string type names to the integer constants. */ public static $types = array( 'string' => self::C_STRING, 'istring' => self::ISTRING, 'text' => self::TEXT, 'itext' => self::ITEXT, 'int' => self::C_INT, 'float' => self::C_FLOAT, 'bool' => self::C_BOOL, 'lookup' => self::LOOKUP, 'list' => self::ALIST, 'hash' => self::HASH, 'mixed' => self::C_MIXED ); /** * Lookup table of types that are string, and can have aliases or * allowed value lists. */ public static $stringTypes = array( self::C_STRING => true, self::ISTRING => true, self::TEXT => true, self::ITEXT => true, ); /** * Validate a variable according to type. * It may return NULL as a valid type if $allow_null is true. * * @param mixed $var Variable to validate * @param int $type Type of variable, see HTMLPurifier_VarParser->types * @param bool $allow_null Whether or not to permit null as a value * @return string Validated and type-coerced variable * @throws HTMLPurifier_VarParserException */ final public function parse($var, $type, $allow_null = false) { if (is_string($type)) { if (!isset(HTMLPurifier_VarParser::$types[$type])) { throw new HTMLPurifier_VarParserException("Invalid type '$type'"); } else { $type = HTMLPurifier_VarParser::$types[$type]; } } $var = $this->parseImplementation($var, $type, $allow_null); if ($allow_null && $var === null) { return null; } // These are basic checks, to make sure nothing horribly wrong // happened in our implementations. switch ($type) { case (self::C_STRING): case (self::ISTRING): case (self::TEXT): case (self::ITEXT): if (!is_string($var)) { break; } if ($type == self::ISTRING || $type == self::ITEXT) { $var = strtolower($var); } return $var; case (self::C_INT): if (!is_int($var)) { break; } return $var; case (self::C_FLOAT): if (!is_float($var)) { break; } return $var; case (self::C_BOOL): if (!is_bool($var)) { break; } return $var; case (self::LOOKUP): case (self::ALIST): case (self::HASH): if (!is_array($var)) { break; } if ($type === self::LOOKUP) { foreach ($var as $k) { if ($k !== true) { $this->error('Lookup table contains value other than true'); } } } elseif ($type === self::ALIST) { $keys = array_keys($var); if (array_keys($keys) !== $keys) { $this->error('Indices for list are not uniform'); } } return $var; case (self::C_MIXED): return $var; default: $this->errorInconsistent(get_class($this), $type); } $this->errorGeneric($var, $type); } /** * Actually implements the parsing. Base implementation does not * do anything to $var. Subclasses should overload this! * @param mixed $var * @param int $type * @param bool $allow_null * @return string */ protected function parseImplementation($var, $type, $allow_null) { return $var; } /** * Throws an exception. * @throws HTMLPurifier_VarParserException */ protected function error($msg) { throw new HTMLPurifier_VarParserException($msg); } /** * Throws an inconsistency exception. * @note This should not ever be called. It would be called if we * extend the allowed values of HTMLPurifier_VarParser without * updating subclasses. * @param string $class * @param int $type * @throws HTMLPurifier_Exception */ protected function errorInconsistent($class, $type) { throw new HTMLPurifier_Exception( "Inconsistency in $class: " . HTMLPurifier_VarParser::getTypeName($type) . " not implemented" ); } /** * Generic error for if a type didn't work. * @param mixed $var * @param int $type */ protected function errorGeneric($var, $type) { $vtype = gettype($var); $this->error("Expected type " . HTMLPurifier_VarParser::getTypeName($type) . ", got $vtype"); } /** * @param int $type * @return string */ public static function getTypeName($type) { static $lookup; if (!$lookup) { // Lazy load the alternative lookup table $lookup = array_flip(HTMLPurifier_VarParser::$types); } if (!isset($lookup[$type])) { return 'unknown'; } return $lookup[$type]; } } // vim: et sw=4 sts=4 HTMLPurifier/EntityLookup.php 0000644 00000002622 15111210766 0012177 0 ustar 00 <?php /** * Object that provides entity lookup table from entity name to character */ class HTMLPurifier_EntityLookup { /** * Assoc array of entity name to character represented. * @type array */ public $table; /** * Sets up the entity lookup table from the serialized file contents. * @param bool $file * @note The serialized contents are versioned, but were generated * using the maintenance script generate_entity_file.php * @warning This is not in constructor to help enforce the Singleton */ public function setup($file = false) { if (!$file) { $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser'; } $this->table = unserialize(file_get_contents($file)); } /** * Retrieves sole instance of the object. * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. * @return HTMLPurifier_EntityLookup */ public static function instance($prototype = false) { // no references, since PHP doesn't copy unless modified static $instance = null; if ($prototype) { $instance = $prototype; } elseif (!$instance) { $instance = new HTMLPurifier_EntityLookup(); $instance->setup(); } return $instance; } } // vim: et sw=4 sts=4 HTMLPurifier/Queue.php 0000644 00000003017 15111210766 0010614 0 ustar 00 <?php /** * A simple array-backed queue, based off of the classic Okasaki * persistent amortized queue. The basic idea is to maintain two * stacks: an input stack and an output stack. When the output * stack runs out, reverse the input stack and use it as the output * stack. * * We don't use the SPL implementation because it's only supported * on PHP 5.3 and later. * * Exercise: Prove that push/pop on this queue take amortized O(1) time. * * Exercise: Extend this queue to be a deque, while preserving amortized * O(1) time. Some care must be taken on rebalancing to avoid quadratic * behaviour caused by repeatedly shuffling data from the input stack * to the output stack and back. */ class HTMLPurifier_Queue { private $input; private $output; public function __construct($input = array()) { $this->input = $input; $this->output = array(); } /** * Shifts an element off the front of the queue. */ public function shift() { if (empty($this->output)) { $this->output = array_reverse($this->input); $this->input = array(); } if (empty($this->output)) { return NULL; } return array_pop($this->output); } /** * Pushes an element onto the front of the queue. */ public function push($x) { array_push($this->input, $x); } /** * Checks if it's empty. */ public function isEmpty() { return empty($this->input) && empty($this->output); } } HTMLPurifier/VarParserException.php 0000644 00000000235 15111210766 0013313 0 ustar 00 <?php /** * Exception type for HTMLPurifier_VarParser */ class HTMLPurifier_VarParserException extends HTMLPurifier_Exception { } // vim: et sw=4 sts=4 HTMLPurifier/AttrCollections.php 0000644 00000011376 15111210766 0012650 0 ustar 00 <?php /** * Defines common attribute collections that modules reference */ class HTMLPurifier_AttrCollections { /** * Associative array of attribute collections, indexed by name. * @type array */ public $info = array(); /** * Performs all expansions on internal data for use by other inclusions * It also collects all attribute collection extensions from * modules * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance * @param HTMLPurifier_HTMLModule[] $modules Hash array of HTMLPurifier_HTMLModule members */ public function __construct($attr_types, $modules) { $this->doConstruct($attr_types, $modules); } public function doConstruct($attr_types, $modules) { // load extensions from the modules foreach ($modules as $module) { foreach ($module->attr_collections as $coll_i => $coll) { if (!isset($this->info[$coll_i])) { $this->info[$coll_i] = array(); } foreach ($coll as $attr_i => $attr) { if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { // merge in includes $this->info[$coll_i][$attr_i] = array_merge( $this->info[$coll_i][$attr_i], $attr ); continue; } $this->info[$coll_i][$attr_i] = $attr; } } } // perform internal expansions and inclusions foreach ($this->info as $name => $attr) { // merge attribute collections that include others $this->performInclusions($this->info[$name]); // replace string identifiers with actual attribute objects $this->expandIdentifiers($this->info[$name], $attr_types); } } /** * Takes a reference to an attribute associative array and performs * all inclusions specified by the zero index. * @param array &$attr Reference to attribute array */ public function performInclusions(&$attr) { if (!isset($attr[0])) { return; } $merge = $attr[0]; $seen = array(); // recursion guard // loop through all the inclusions for ($i = 0; isset($merge[$i]); $i++) { if (isset($seen[$merge[$i]])) { continue; } $seen[$merge[$i]] = true; // foreach attribute of the inclusion, copy it over if (!isset($this->info[$merge[$i]])) { continue; } foreach ($this->info[$merge[$i]] as $key => $value) { if (isset($attr[$key])) { continue; } // also catches more inclusions $attr[$key] = $value; } if (isset($this->info[$merge[$i]][0])) { // recursion $merge = array_merge($merge, $this->info[$merge[$i]][0]); } } unset($attr[0]); } /** * Expands all string identifiers in an attribute array by replacing * them with the appropriate values inside HTMLPurifier_AttrTypes * @param array &$attr Reference to attribute array * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance */ public function expandIdentifiers(&$attr, $attr_types) { // because foreach will process new elements we add, make sure we // skip duplicates $processed = array(); foreach ($attr as $def_i => $def) { // skip inclusions if ($def_i === 0) { continue; } if (isset($processed[$def_i])) { continue; } // determine whether or not attribute is required if ($required = (strpos($def_i, '*') !== false)) { // rename the definition unset($attr[$def_i]); $def_i = trim($def_i, '*'); $attr[$def_i] = $def; } $processed[$def_i] = true; // if we've already got a literal object, move on if (is_object($def)) { // preserve previous required $attr[$def_i]->required = ($required || $attr[$def_i]->required); continue; } if ($def === false) { unset($attr[$def_i]); continue; } if ($t = $attr_types->get($def)) { $attr[$def_i] = $t; $attr[$def_i]->required = $required; } else { unset($attr[$def_i]); } } } } // vim: et sw=4 sts=4 HTMLPurifier/URIScheme.php 0000644 00000006631 15111210766 0011321 0 ustar 00 <?php /** * Validator for the components of a URI for a specific scheme */ abstract class HTMLPurifier_URIScheme { /** * Scheme's default port (integer). If an explicit port number is * specified that coincides with the default port, it will be * elided. * @type int */ public $default_port = null; /** * Whether or not URIs of this scheme are locatable by a browser * http and ftp are accessible, while mailto and news are not. * @type bool */ public $browsable = false; /** * Whether or not data transmitted over this scheme is encrypted. * https is secure, http is not. * @type bool */ public $secure = false; /** * Whether or not the URI always uses <hier_part>, resolves edge cases * with making relative URIs absolute * @type bool */ public $hierarchical = false; /** * Whether or not the URI may omit a hostname when the scheme is * explicitly specified, ala file:///path/to/file. As of writing, * 'file' is the only scheme that browsers support his properly. * @type bool */ public $may_omit_host = false; /** * Validates the components of a URI for a specific scheme. * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool success or failure */ abstract public function doValidate(&$uri, $config, $context); /** * Public interface for validating components of a URI. Performs a * bunch of default actions. Don't overload this method. * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool success or failure */ public function validate(&$uri, $config, $context) { if ($this->default_port == $uri->port) { $uri->port = null; } // kludge: browsers do funny things when the scheme but not the // authority is set if (!$this->may_omit_host && // if the scheme is present, a missing host is always in error (!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) || // if the scheme is not present, a *blank* host is in error, // since this translates into '///path' which most browsers // interpret as being 'http://path'. (is_null($uri->scheme) && $uri->host === '') ) { do { if (is_null($uri->scheme)) { if (substr($uri->path, 0, 2) != '//') { $uri->host = null; break; } // URI is '////path', so we cannot nullify the // host to preserve semantics. Try expanding the // hostname instead (fall through) } // first see if we can manually insert a hostname $host = $config->get('URI.Host'); if (!is_null($host)) { $uri->host = $host; } else { // we can't do anything sensible, reject the URL. return false; } } while (false); } return $this->doValidate($uri, $config, $context); } } // vim: et sw=4 sts=4 HTMLPurifier/URISchemeRegistry.php 0000644 00000004551 15111210766 0013051 0 ustar 00 <?php /** * Registry for retrieving specific URI scheme validator objects. */ class HTMLPurifier_URISchemeRegistry { /** * Retrieve sole instance of the registry. * @param HTMLPurifier_URISchemeRegistry $prototype Optional prototype to overload sole instance with, * or bool true to reset to default registry. * @return HTMLPurifier_URISchemeRegistry * @note Pass a registry object $prototype with a compatible interface and * the function will copy it and return it all further times. */ public static function instance($prototype = null) { static $instance = null; if ($prototype !== null) { $instance = $prototype; } elseif ($instance === null || $prototype == true) { $instance = new HTMLPurifier_URISchemeRegistry(); } return $instance; } /** * Cache of retrieved schemes. * @type HTMLPurifier_URIScheme[] */ protected $schemes = array(); /** * Retrieves a scheme validator object * @param string $scheme String scheme name like http or mailto * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_URIScheme */ public function getScheme($scheme, $config, $context) { if (!$config) { $config = HTMLPurifier_Config::createDefault(); } // important, otherwise attacker could include arbitrary file $allowed_schemes = $config->get('URI.AllowedSchemes'); if (!$config->get('URI.OverrideAllowedSchemes') && !isset($allowed_schemes[$scheme]) ) { return; } if (isset($this->schemes[$scheme])) { return $this->schemes[$scheme]; } if (!isset($allowed_schemes[$scheme])) { return; } $class = 'HTMLPurifier_URIScheme_' . $scheme; if (!class_exists($class)) { return; } $this->schemes[$scheme] = new $class(); return $this->schemes[$scheme]; } /** * Registers a custom scheme to the cache, bypassing reflection. * @param string $scheme Scheme name * @param HTMLPurifier_URIScheme $scheme_obj */ public function register($scheme, $scheme_obj) { $this->schemes[$scheme] = $scheme_obj; } } // vim: et sw=4 sts=4 HTMLPurifier/Bootstrap.php 0000644 00000011005 15111210766 0011501 0 ustar 00 <?php // constants are slow, so we use as few as possible if (!defined('HTMLPURIFIER_PREFIX')) { define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/..')); } // accomodations for versions earlier than 5.0.2 // borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net> if (!defined('PHP_EOL')) { switch (strtoupper(substr(PHP_OS, 0, 3))) { case 'WIN': define('PHP_EOL', "\r\n"); break; case 'DAR': define('PHP_EOL', "\r"); break; default: define('PHP_EOL', "\n"); } } /** * Bootstrap class that contains meta-functionality for HTML Purifier such as * the autoload function. * * @note * This class may be used without any other files from HTML Purifier. */ class HTMLPurifier_Bootstrap { /** * Autoload function for HTML Purifier * @param string $class Class to load * @return bool */ public static function autoload($class) { $file = HTMLPurifier_Bootstrap::getPath($class); if (!$file) { return false; } // Technically speaking, it should be ok and more efficient to // just do 'require', but Antonio Parraga reports that with // Zend extensions such as Zend debugger and APC, this invariant // may be broken. Since we have efficient alternatives, pay // the cost here and avoid the bug. require_once HTMLPURIFIER_PREFIX . '/' . $file; return true; } /** * Returns the path for a specific class. * @param string $class Class path to get * @return string */ public static function getPath($class) { if (strncmp('HTMLPurifier', $class, 12) !== 0) { return false; } // Custom implementations if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { $code = str_replace('_', '-', substr($class, 22)); $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; } else { $file = str_replace('_', '/', $class) . '.php'; } if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { return false; } return $file; } /** * "Pre-registers" our autoloader on the SPL stack. */ public static function registerAutoload() { $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); if (($funcs = spl_autoload_functions()) === false) { spl_autoload_register($autoload); } elseif (function_exists('spl_autoload_unregister')) { if (version_compare(PHP_VERSION, '5.3.0', '>=')) { // prepend flag exists, no need for shenanigans spl_autoload_register($autoload, true, true); } else { $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && version_compare(PHP_VERSION, '5.1.0', '>='); foreach ($funcs as $func) { if ($buggy && is_array($func)) { // :TRICKY: There are some compatibility issues and some // places where we need to error out $reflector = new ReflectionMethod($func[0], $func[1]); if (!$reflector->isStatic()) { throw new Exception( 'HTML Purifier autoloader registrar is not compatible with non-static object methods due to PHP Bug #44144; Please do not use HTMLPurifier.autoload.php (or any file that includes this file); instead, place the code: spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) after your own autoloaders.' ); } // Suprisingly, spl_autoload_register supports the // Class::staticMethod callback format, although call_user_func doesn't if ($compat) { $func = implode('::', $func); } } spl_autoload_unregister($func); } spl_autoload_register($autoload); foreach ($funcs as $func) { spl_autoload_register($func); } } } } } // vim: et sw=4 sts=4 HTMLPurifier/ElementDef.php 0000644 00000016553 15111210766 0011551 0 ustar 00 <?php /** * Structure that stores an HTML element definition. Used by * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule. * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition. * Please update that class too. * @warning If you add new properties to this class, you MUST update * the mergeIn() method. */ class HTMLPurifier_ElementDef { /** * Does the definition work by itself, or is it created solely * for the purpose of merging into another definition? * @type bool */ public $standalone = true; /** * Associative array of attribute name to HTMLPurifier_AttrDef. * @type array * @note Before being processed by HTMLPurifier_AttrCollections * when modules are finalized during * HTMLPurifier_HTMLDefinition->setup(), this array may also * contain an array at index 0 that indicates which attribute * collections to load into the full array. It may also * contain string indentifiers in lieu of HTMLPurifier_AttrDef, * see HTMLPurifier_AttrTypes on how they are expanded during * HTMLPurifier_HTMLDefinition->setup() processing. */ public $attr = array(); // XXX: Design note: currently, it's not possible to override // previously defined AttrTransforms without messing around with // the final generated config. This is by design; a previous version // used an associated list of attr_transform, but it was extremely // easy to accidentally override other attribute transforms by // forgetting to specify an index (and just using 0.) While we // could check this by checking the index number and complaining, // there is a second problem which is that it is not at all easy to // tell when something is getting overridden. Combine this with a // codebase where this isn't really being used, and it's perfect for // nuking. /** * List of tags HTMLPurifier_AttrTransform to be done before validation. * @type array */ public $attr_transform_pre = array(); /** * List of tags HTMLPurifier_AttrTransform to be done after validation. * @type array */ public $attr_transform_post = array(); /** * HTMLPurifier_ChildDef of this tag. * @type HTMLPurifier_ChildDef */ public $child; /** * Abstract string representation of internal ChildDef rules. * @see HTMLPurifier_ContentSets for how this is parsed and then transformed * into an HTMLPurifier_ChildDef. * @warning This is a temporary variable that is not available after * being processed by HTMLDefinition * @type string */ public $content_model; /** * Value of $child->type, used to determine which ChildDef to use, * used in combination with $content_model. * @warning This must be lowercase * @warning This is a temporary variable that is not available after * being processed by HTMLDefinition * @type string */ public $content_model_type; /** * Does the element have a content model (#PCDATA | Inline)*? This * is important for chameleon ins and del processing in * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't * have to worry about this one. * @type bool */ public $descendants_are_inline = false; /** * List of the names of required attributes this element has. * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() * @type array */ public $required_attr = array(); /** * Lookup table of tags excluded from all descendants of this tag. * @type array * @note SGML permits exclusions for all descendants, but this is * not possible with DTDs or XML Schemas. W3C has elected to * use complicated compositions of content_models to simulate * exclusion for children, but we go the simpler, SGML-style * route of flat-out exclusions, which correctly apply to * all descendants and not just children. Note that the XHTML * Modularization Abstract Modules are blithely unaware of such * distinctions. */ public $excludes = array(); /** * This tag is explicitly auto-closed by the following tags. * @type array */ public $autoclose = array(); /** * If a foreign element is found in this element, test if it is * allowed by this sub-element; if it is, instead of closing the * current element, place it inside this element. * @type string */ public $wrap; /** * Whether or not this is a formatting element affected by the * "Active Formatting Elements" algorithm. * @type bool */ public $formatting; /** * Low-level factory constructor for creating new standalone element defs */ public static function create($content_model, $content_model_type, $attr) { $def = new HTMLPurifier_ElementDef(); $def->content_model = $content_model; $def->content_model_type = $content_model_type; $def->attr = $attr; return $def; } /** * Merges the values of another element definition into this one. * Values from the new element def take precedence if a value is * not mergeable. * @param HTMLPurifier_ElementDef $def */ public function mergeIn($def) { // later keys takes precedence foreach ($def->attr as $k => $v) { if ($k === 0) { // merge in the includes // sorry, no way to override an include foreach ($v as $v2) { $this->attr[0][] = $v2; } continue; } if ($v === false) { if (isset($this->attr[$k])) { unset($this->attr[$k]); } continue; } $this->attr[$k] = $v; } $this->_mergeAssocArray($this->excludes, $def->excludes); $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); if (!empty($def->content_model)) { $this->content_model = str_replace("#SUPER", (string)$this->content_model, $def->content_model); $this->child = false; } if (!empty($def->content_model_type)) { $this->content_model_type = $def->content_model_type; $this->child = false; } if (!is_null($def->child)) { $this->child = $def->child; } if (!is_null($def->formatting)) { $this->formatting = $def->formatting; } if ($def->descendants_are_inline) { $this->descendants_are_inline = $def->descendants_are_inline; } } /** * Merges one array into another, removes values which equal false * @param $a1 Array by reference that is merged into * @param $a2 Array that merges into $a1 */ private function _mergeAssocArray(&$a1, $a2) { foreach ($a2 as $k => $v) { if ($v === false) { if (isset($a1[$k])) { unset($a1[$k]); } continue; } $a1[$k] = $v; } } } // vim: et sw=4 sts=4 HTMLPurifier/TokenFactory.php 0000644 00000006033 15111210766 0012141 0 ustar 00 <?php /** * Factory for token generation. * * @note Doing some benchmarking indicates that the new operator is much * slower than the clone operator (even discounting the cost of the * constructor). This class is for that optimization. * Other then that, there's not much point as we don't * maintain parallel HTMLPurifier_Token hierarchies (the main reason why * you'd want to use an abstract factory). * @todo Port DirectLex to use this */ class HTMLPurifier_TokenFactory { // p stands for prototype /** * @type HTMLPurifier_Token_Start */ private $p_start; /** * @type HTMLPurifier_Token_End */ private $p_end; /** * @type HTMLPurifier_Token_Empty */ private $p_empty; /** * @type HTMLPurifier_Token_Text */ private $p_text; /** * @type HTMLPurifier_Token_Comment */ private $p_comment; /** * Generates blank prototypes for cloning. */ public function __construct() { $this->p_start = new HTMLPurifier_Token_Start('', array()); $this->p_end = new HTMLPurifier_Token_End(''); $this->p_empty = new HTMLPurifier_Token_Empty('', array()); $this->p_text = new HTMLPurifier_Token_Text(''); $this->p_comment = new HTMLPurifier_Token_Comment(''); } /** * Creates a HTMLPurifier_Token_Start. * @param string $name Tag name * @param array $attr Associative array of attributes * @return HTMLPurifier_Token_Start Generated HTMLPurifier_Token_Start */ public function createStart($name, $attr = array()) { $p = clone $this->p_start; $p->__construct($name, $attr); return $p; } /** * Creates a HTMLPurifier_Token_End. * @param string $name Tag name * @return HTMLPurifier_Token_End Generated HTMLPurifier_Token_End */ public function createEnd($name) { $p = clone $this->p_end; $p->__construct($name); return $p; } /** * Creates a HTMLPurifier_Token_Empty. * @param string $name Tag name * @param array $attr Associative array of attributes * @return HTMLPurifier_Token_Empty Generated HTMLPurifier_Token_Empty */ public function createEmpty($name, $attr = array()) { $p = clone $this->p_empty; $p->__construct($name, $attr); return $p; } /** * Creates a HTMLPurifier_Token_Text. * @param string $data Data of text token * @return HTMLPurifier_Token_Text Generated HTMLPurifier_Token_Text */ public function createText($data) { $p = clone $this->p_text; $p->__construct($data); return $p; } /** * Creates a HTMLPurifier_Token_Comment. * @param string $data Data of comment token * @return HTMLPurifier_Token_Comment Generated HTMLPurifier_Token_Comment */ public function createComment($data) { $p = clone $this->p_comment; $p->__construct($data); return $p; } } // vim: et sw=4 sts=4 HTMLPurifier/Filter/ExtractStyleBlocks.php 0000644 00000032444 15111210766 0014554 0 ustar 00 <?php // why is this a top level function? Because PHP 5.2.0 doesn't seem to // understand how to interpret this filter if it's a static method. // It's all really silly, but if we go this route it might be reasonable // to coalesce all of these methods into one. function htmlpurifier_filter_extractstyleblocks_muteerrorhandler() { } /** * This filter extracts <style> blocks from input HTML, cleans them up * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') * so they can be used elsewhere in the document. * * @note * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for * sample usage. * * @note * This filter can also be used on stylesheets not included in the * document--something purists would probably prefer. Just directly * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() */ class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter { /** * @type string */ public $name = 'ExtractStyleBlocks'; /** * @type array */ private $_styleMatches = array(); /** * @type csstidy */ private $_tidy; /** * @type HTMLPurifier_AttrDef_HTML_ID */ private $_id_attrdef; /** * @type HTMLPurifier_AttrDef_CSS_Ident */ private $_class_attrdef; /** * @type HTMLPurifier_AttrDef_Enum */ private $_enum_attrdef; public function __construct() { $this->_tidy = new csstidy(); $this->_tidy->set_cfg('lowercase_s', false); $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( array( 'first-child', 'link', 'visited', 'active', 'hover', 'focus' ) ); } /** * Save the contents of CSS blocks to style matches * @param array $matches preg_replace style $matches array */ protected function styleCallback($matches) { $this->_styleMatches[] = $matches[1]; } /** * Removes inline <style> tags from HTML, saves them for later use * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return string * @todo Extend to indicate non-text/css style blocks */ public function preFilter($html, $config, $context) { $tidy = $config->get('Filter.ExtractStyleBlocks.TidyImpl'); if ($tidy !== null) { $this->_tidy = $tidy; } // NB: this must be NON-greedy because if we have // <style>foo</style> <style>bar</style> // we must not grab foo</style> <style>bar $html = preg_replace_callback('#<style(?:\s.*)?>(.*)<\/style>#isU', array($this, 'styleCallback'), $html); $style_blocks = $this->_styleMatches; $this->_styleMatches = array(); // reset $context->register('StyleBlocks', $style_blocks); // $context must not be reused if ($this->_tidy) { foreach ($style_blocks as &$style) { $style = $this->cleanCSS($style, $config, $context); } } return $html; } /** * Takes CSS (the stuff found in <style>) and cleans it. * @warning Requires CSSTidy <http://csstidy.sourceforge.net/> * @param string $css CSS styling to clean * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @throws HTMLPurifier_Exception * @return string Cleaned CSS */ public function cleanCSS($css, $config, $context) { // prepare scope $scope = $config->get('Filter.ExtractStyleBlocks.Scope'); if ($scope !== null) { $scopes = array_map('trim', explode(',', $scope)); } else { $scopes = array(); } // remove comments from CSS $css = trim($css); if (strncmp('<!--', $css, 4) === 0) { $css = substr($css, 4); } if (strlen($css) > 3 && substr($css, -3) == '-->') { $css = substr($css, 0, -3); } $css = trim($css); set_error_handler('htmlpurifier_filter_extractstyleblocks_muteerrorhandler'); $this->_tidy->parse($css); restore_error_handler(); $css_definition = $config->getDefinition('CSS'); $html_definition = $config->getDefinition('HTML'); $new_css = array(); foreach ($this->_tidy->css as $k => $decls) { // $decls are all CSS declarations inside an @ selector $new_decls = array(); foreach ($decls as $selector => $style) { $selector = trim($selector); if ($selector === '') { continue; } // should not happen // Parse the selector // Here is the relevant part of the CSS grammar: // // ruleset // : selector [ ',' S* selector ]* '{' ... // selector // : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]? // combinator // : '+' S* // : '>' S* // simple_selector // : element_name [ HASH | class | attrib | pseudo ]* // | [ HASH | class | attrib | pseudo ]+ // element_name // : IDENT | '*' // ; // class // : '.' IDENT // ; // attrib // : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* // [ IDENT | STRING ] S* ]? ']' // ; // pseudo // : ':' [ IDENT | FUNCTION S* [IDENT S*]? ')' ] // ; // // For reference, here are the relevant tokens: // // HASH #{name} // IDENT {ident} // INCLUDES == // DASHMATCH |= // STRING {string} // FUNCTION {ident}\( // // And the lexical scanner tokens // // name {nmchar}+ // nmchar [_a-z0-9-]|{nonascii}|{escape} // nonascii [\240-\377] // escape {unicode}|\\[^\r\n\f0-9a-f] // unicode \\{h}}{1,6}(\r\n|[ \t\r\n\f])? // ident -?{nmstart}{nmchar*} // nmstart [_a-z]|{nonascii}|{escape} // string {string1}|{string2} // string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\" // string2 \'([^\n\r\f\\"]|\\{nl}|{escape})*\' // // We'll implement a subset (in order to reduce attack // surface); in particular: // // - No Unicode support // - No escapes support // - No string support (by proxy no attrib support) // - element_name is matched against allowed // elements (some people might find this // annoying...) // - Pseudo-elements one of :first-child, :link, // :visited, :active, :hover, :focus // handle ruleset $selectors = array_map('trim', explode(',', $selector)); $new_selectors = array(); foreach ($selectors as $sel) { // split on +, > and spaces $basic_selectors = preg_split('/\s*([+> ])\s*/', $sel, -1, PREG_SPLIT_DELIM_CAPTURE); // even indices are chunks, odd indices are // delimiters $nsel = null; $delim = null; // guaranteed to be non-null after // two loop iterations for ($i = 0, $c = count($basic_selectors); $i < $c; $i++) { $x = $basic_selectors[$i]; if ($i % 2) { // delimiter if ($x === ' ') { $delim = ' '; } else { $delim = ' ' . $x . ' '; } } else { // simple selector $components = preg_split('/([#.:])/', $x, -1, PREG_SPLIT_DELIM_CAPTURE); $sdelim = null; $nx = null; for ($j = 0, $cc = count($components); $j < $cc; $j++) { $y = $components[$j]; if ($j === 0) { if ($y === '*' || isset($html_definition->info[$y = strtolower($y)])) { $nx = $y; } else { // $nx stays null; this matters // if we don't manage to find // any valid selector content, // in which case we ignore the // outer $delim } } elseif ($j % 2) { // set delimiter $sdelim = $y; } else { $attrdef = null; if ($sdelim === '#') { $attrdef = $this->_id_attrdef; } elseif ($sdelim === '.') { $attrdef = $this->_class_attrdef; } elseif ($sdelim === ':') { $attrdef = $this->_enum_attrdef; } else { throw new HTMLPurifier_Exception('broken invariant sdelim and preg_split'); } $r = $attrdef->validate($y, $config, $context); if ($r !== false) { if ($r !== true) { $y = $r; } if ($nx === null) { $nx = ''; } $nx .= $sdelim . $y; } } } if ($nx !== null) { if ($nsel === null) { $nsel = $nx; } else { $nsel .= $delim . $nx; } } else { // delimiters to the left of invalid // basic selector ignored } } } if ($nsel !== null) { if (!empty($scopes)) { foreach ($scopes as $s) { $new_selectors[] = "$s $nsel"; } } else { $new_selectors[] = $nsel; } } } if (empty($new_selectors)) { continue; } $selector = implode(', ', $new_selectors); foreach ($style as $name => $value) { if (!isset($css_definition->info[$name])) { unset($style[$name]); continue; } $def = $css_definition->info[$name]; $ret = $def->validate($value, $config, $context); if ($ret === false) { unset($style[$name]); } else { $style[$name] = $ret; } } $new_decls[$selector] = $style; } $new_css[$k] = $new_decls; } // remove stuff that shouldn't be used, could be reenabled // after security risks are analyzed $this->_tidy->css = $new_css; $this->_tidy->import = array(); $this->_tidy->charset = null; $this->_tidy->namespace = null; $css = $this->_tidy->print->plain(); // we are going to escape any special characters <>& to ensure // that no funny business occurs (i.e. </style> in a font-family prop). if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { $css = str_replace( array('<', '>', '&'), array('\3C ', '\3E ', '\26 '), $css ); } return $css; } } // vim: et sw=4 sts=4 HTMLPurifier/Filter/YouTube.php 0000644 00000003452 15111210767 0012355 0 ustar 00 <?php class HTMLPurifier_Filter_YouTube extends HTMLPurifier_Filter { /** * @type string */ public $name = 'YouTube'; /** * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return string */ public function preFilter($html, $config, $context) { $pre_regex = '#<object[^>]+>.+?' . '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?</object>#s'; $pre_replace = '<span class="youtube-embed">\1</span>'; return preg_replace($pre_regex, $pre_replace, $html); } /** * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return string */ public function postFilter($html, $config, $context) { $post_regex = '#<span class="youtube-embed">((?:v|cp)/[A-Za-z0-9\-_=]+)</span>#'; return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); } /** * @param $url * @return string */ protected function armorUrl($url) { return str_replace('--', '--', $url); } /** * @param array $matches * @return string */ protected function postFilterCallback($matches) { $url = $this->armorUrl($matches[1]); return '<object width="425" height="350" type="application/x-shockwave-flash" ' . 'data="//www.youtube.com/' . $url . '">' . '<param name="movie" value="//www.youtube.com/' . $url . '"></param>' . '<!--[if IE]>' . '<embed src="//www.youtube.com/' . $url . '"' . 'type="application/x-shockwave-flash"' . 'wmode="transparent" width="425" height="350" />' . '<![endif]-->' . '</object>'; } } // vim: et sw=4 sts=4 HTMLPurifier/Node.php 0000644 00000002400 15111210767 0010411 0 ustar 00 <?php /** * Abstract base node class that all others inherit from. * * Why do we not use the DOM extension? (1) It is not always available, * (2) it has funny constraints on the data it can represent, * whereas we want a maximally flexible representation, and (3) its * interface is a bit cumbersome. */ abstract class HTMLPurifier_Node { /** * Line number of the start token in the source document * @type int */ public $line; /** * Column number of the start token in the source document. Null if unknown. * @type int */ public $col; /** * Lookup array of processing that this token is exempt from. * Currently, valid values are "ValidateAttributes". * @type array */ public $armor = array(); /** * When true, this node should be ignored as non-existent. * * Who is responsible for ignoring dead nodes? FixNesting is * responsible for removing them before passing on to child * validators. */ public $dead = false; /** * Returns a pair of start and end tokens, where the end token * is null if it is not necessary. Does not include children. * @type array */ abstract public function toTokenPair(); } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Serializer/README 0000644 00000000140 15111210767 0015017 0 ustar 00 This is a dummy file to prevent Git from ignoring this empty directory. vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Decorator/Template.php.in 0000644 00000003211 15111210767 0016643 0 ustar 00 <?php require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; /** * Definition cache decorator template. */ class HTMLPurifier_DefinitionCache_Decorator_Template extends HTMLPurifier_DefinitionCache_Decorator { /** * @type string */ public $name = 'Template'; // replace this public function copy() { // replace class name with yours return new HTMLPurifier_DefinitionCache_Decorator_Template(); } // remove methods you don't need /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function add($def, $config) { return parent::add($def, $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function set($def, $config) { return parent::set($def, $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function replace($def, $config) { return parent::replace($def, $config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function get($config) { return parent::get($config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function flush($config) { return parent::flush($config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function cleanup($config) { return parent::cleanup($config); } } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Decorator/Memory.php 0000644 00000004012 15111210767 0015733 0 ustar 00 <?php /** * Definition cache decorator class that saves all cache retrievals * to PHP's memory; good for unit tests or circumstances where * there are lots of configuration objects floating around. */ class HTMLPurifier_DefinitionCache_Decorator_Memory extends HTMLPurifier_DefinitionCache_Decorator { /** * @type array */ protected $definitions; /** * @type string */ public $name = 'Memory'; /** * @return HTMLPurifier_DefinitionCache_Decorator_Memory */ public function copy() { return new HTMLPurifier_DefinitionCache_Decorator_Memory(); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function add($def, $config) { $status = parent::add($def, $config); if ($status) { $this->definitions[$this->generateKey($config)] = $def; } return $status; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function set($def, $config) { $status = parent::set($def, $config); if ($status) { $this->definitions[$this->generateKey($config)] = $def; } return $status; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function replace($def, $config) { $status = parent::replace($def, $config); if ($status) { $this->definitions[$this->generateKey($config)] = $def; } return $status; } /** * @param HTMLPurifier_Config $config * @return mixed */ public function get($config) { $key = $this->generateKey($config); if (isset($this->definitions[$key])) { return $this->definitions[$key]; } $this->definitions[$key] = parent::get($config); return $this->definitions[$key]; } } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Decorator/Cleanup.php 0000644 00000003243 15111210767 0016057 0 ustar 00 <?php /** * Definition cache decorator class that cleans up the cache * whenever there is a cache miss. */ class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends HTMLPurifier_DefinitionCache_Decorator { /** * @type string */ public $name = 'Cleanup'; /** * @return HTMLPurifier_DefinitionCache_Decorator_Cleanup */ public function copy() { return new HTMLPurifier_DefinitionCache_Decorator_Cleanup(); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function add($def, $config) { $status = parent::add($def, $config); if (!$status) { parent::cleanup($config); } return $status; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function set($def, $config) { $status = parent::set($def, $config); if (!$status) { parent::cleanup($config); } return $status; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function replace($def, $config) { $status = parent::replace($def, $config); if (!$status) { parent::cleanup($config); } return $status; } /** * @param HTMLPurifier_Config $config * @return mixed */ public function get($config) { $ret = parent::get($config); if (!$ret) { parent::cleanup($config); } return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Decorator.php 0000644 00000004513 15111210767 0014471 0 ustar 00 <?php class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache { /** * Cache object we are decorating * @type HTMLPurifier_DefinitionCache */ public $cache; /** * The name of the decorator * @var string */ public $name; public function __construct() { } /** * Lazy decorator function * @param HTMLPurifier_DefinitionCache $cache Reference to cache object to decorate * @return HTMLPurifier_DefinitionCache_Decorator */ public function decorate(&$cache) { $decorator = $this->copy(); // reference is necessary for mocks in PHP 4 $decorator->cache =& $cache; $decorator->type = $cache->type; return $decorator; } /** * Cross-compatible clone substitute * @return HTMLPurifier_DefinitionCache_Decorator */ public function copy() { return new HTMLPurifier_DefinitionCache_Decorator(); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function add($def, $config) { return $this->cache->add($def, $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function set($def, $config) { return $this->cache->set($def, $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return mixed */ public function replace($def, $config) { return $this->cache->replace($def, $config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function get($config) { return $this->cache->get($config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function remove($config) { return $this->cache->remove($config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function flush($config) { return $this->cache->flush($config); } /** * @param HTMLPurifier_Config $config * @return mixed */ public function cleanup($config) { return $this->cache->cleanup($config); } } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Null.php 0000644 00000002510 15111210767 0013454 0 ustar 00 <?php /** * Null cache object to use when no caching is on. */ class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache { /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return bool */ public function add($def, $config) { return false; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return bool */ public function set($def, $config) { return false; } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return bool */ public function replace($def, $config) { return false; } /** * @param HTMLPurifier_Config $config * @return bool */ public function remove($config) { return false; } /** * @param HTMLPurifier_Config $config * @return bool */ public function get($config) { return false; } /** * @param HTMLPurifier_Config $config * @return bool */ public function flush($config) { return false; } /** * @param HTMLPurifier_Config $config * @return bool */ public function cleanup($config) { return false; } } // vim: et sw=4 sts=4 HTMLPurifier/DefinitionCache/Serializer.php 0000644 00000022202 15111210767 0014653 0 ustar 00 <?php class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCache { /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return int|bool */ public function add($def, $config) { if (!$this->checkDefType($def)) { return; } $file = $this->generateFilePath($config); if (file_exists($file)) { return false; } if (!$this->_prepareDir($config)) { return false; } return $this->_write($file, serialize($def), $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return int|bool */ public function set($def, $config) { if (!$this->checkDefType($def)) { return; } $file = $this->generateFilePath($config); if (!$this->_prepareDir($config)) { return false; } return $this->_write($file, serialize($def), $config); } /** * @param HTMLPurifier_Definition $def * @param HTMLPurifier_Config $config * @return int|bool */ public function replace($def, $config) { if (!$this->checkDefType($def)) { return; } $file = $this->generateFilePath($config); if (!file_exists($file)) { return false; } if (!$this->_prepareDir($config)) { return false; } return $this->_write($file, serialize($def), $config); } /** * @param HTMLPurifier_Config $config * @return bool|HTMLPurifier_Config */ public function get($config) { $file = $this->generateFilePath($config); if (!file_exists($file)) { return false; } return unserialize(file_get_contents($file)); } /** * @param HTMLPurifier_Config $config * @return bool */ public function remove($config) { $file = $this->generateFilePath($config); if (!file_exists($file)) { return false; } return unlink($file); } /** * @param HTMLPurifier_Config $config * @return bool */ public function flush($config) { if (!$this->_prepareDir($config)) { return false; } $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); // Apparently, on some versions of PHP, readdir will return // an empty string if you pass an invalid argument to readdir. // So you need this test. See #49. if (false === $dh) { return false; } while (false !== ($filename = readdir($dh))) { if (empty($filename)) { continue; } if ($filename[0] === '.') { continue; } unlink($dir . '/' . $filename); } closedir($dh); return true; } /** * @param HTMLPurifier_Config $config * @return bool */ public function cleanup($config) { if (!$this->_prepareDir($config)) { return false; } $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); // See #49 (and above). if (false === $dh) { return false; } while (false !== ($filename = readdir($dh))) { if (empty($filename)) { continue; } if ($filename[0] === '.') { continue; } $key = substr($filename, 0, strlen($filename) - 4); if ($this->isOld($key, $config)) { unlink($dir . '/' . $filename); } } closedir($dh); return true; } /** * Generates the file path to the serial file corresponding to * the configuration and definition name * @param HTMLPurifier_Config $config * @return string * @todo Make protected */ public function generateFilePath($config) { $key = $this->generateKey($config); return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; } /** * Generates the path to the directory contain this cache's serial files * @param HTMLPurifier_Config $config * @return string * @note No trailing slash * @todo Make protected */ public function generateDirectoryPath($config) { $base = $this->generateBaseDirectoryPath($config); return $base . '/' . $this->type; } /** * Generates path to base directory that contains all definition type * serials * @param HTMLPurifier_Config $config * @return mixed|string * @todo Make protected */ public function generateBaseDirectoryPath($config) { $base = $config->get('Cache.SerializerPath'); $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; return $base; } /** * Convenience wrapper function for file_put_contents * @param string $file File name to write to * @param string $data Data to write into file * @param HTMLPurifier_Config $config * @return int|bool Number of bytes written if success, or false if failure. */ private function _write($file, $data, $config) { $result = file_put_contents($file, $data); if ($result !== false) { // set permissions of the new file (no execute) $chmod = $config->get('Cache.SerializerPermissions'); if ($chmod !== null) { chmod($file, $chmod & 0666); } } return $result; } /** * Prepares the directory that this type stores the serials in * @param HTMLPurifier_Config $config * @return bool True if successful */ private function _prepareDir($config) { $directory = $this->generateDirectoryPath($config); $chmod = $config->get('Cache.SerializerPermissions'); if ($chmod === null) { if (!@mkdir($directory) && !is_dir($directory)) { trigger_error( 'Could not create directory ' . $directory . '', E_USER_WARNING ); return false; } return true; } if (!is_dir($directory)) { $base = $this->generateBaseDirectoryPath($config); if (!is_dir($base)) { trigger_error( 'Base directory ' . $base . ' does not exist, please create or change using %Cache.SerializerPath', E_USER_WARNING ); return false; } elseif (!$this->_testPermissions($base, $chmod)) { return false; } if (!@mkdir($directory, $chmod) && !is_dir($directory)) { trigger_error( 'Could not create directory ' . $directory . '', E_USER_WARNING ); return false; } if (!$this->_testPermissions($directory, $chmod)) { return false; } } elseif (!$this->_testPermissions($directory, $chmod)) { return false; } return true; } /** * Tests permissions on a directory and throws out friendly * error messages and attempts to chmod it itself if possible * @param string $dir Directory path * @param int $chmod Permissions * @return bool True if directory is writable */ private function _testPermissions($dir, $chmod) { // early abort, if it is writable, everything is hunky-dory if (is_writable($dir)) { return true; } if (!is_dir($dir)) { // generally, you'll want to handle this beforehand // so a more specific error message can be given trigger_error( 'Directory ' . $dir . ' does not exist', E_USER_WARNING ); return false; } if (function_exists('posix_getuid') && $chmod !== null) { // POSIX system, we can give more specific advice if (fileowner($dir) === posix_getuid()) { // we can chmod it ourselves $chmod = $chmod | 0700; if (chmod($dir, $chmod)) { return true; } } elseif (filegroup($dir) === posix_getgid()) { $chmod = $chmod | 0070; } else { // PHP's probably running as nobody, so we'll // need to give global permissions $chmod = $chmod | 0777; } trigger_error( 'Directory ' . $dir . ' not writable, ' . 'please chmod to ' . decoct($chmod), E_USER_WARNING ); } else { // generic error message trigger_error( 'Directory ' . $dir . ' not writable, ' . 'please alter file permissions', E_USER_WARNING ); } return false; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Table.php 0000644 00000016000 15111210767 0012216 0 ustar 00 <?php /** * Definition for tables. The general idea is to extract out all of the * essential bits, and then reconstruct it later. * * This is a bit confusing, because the DTDs and the W3C * validators seem to disagree on the appropriate definition. The * DTD claims: * * (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+) * * But actually, the HTML4 spec then has this to say: * * The TBODY start tag is always required except when the table * contains only one table body and no table head or foot sections. * The TBODY end tag may always be safely omitted. * * So the DTD is kind of wrong. The validator is, unfortunately, kind * of on crack. * * The definition changed again in XHTML1.1; and in my opinion, this * formulation makes the most sense. * * caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ )) * * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode. * If we encounter a thead, tfoot or tbody, we are placed in the former * mode, and we *must* wrap any stray tr segments with a tbody. But if * we don't run into any of them, just have tr tags is OK. */ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef { /** * @type bool */ public $allow_empty = false; /** * @type string */ public $type = 'table'; /** * @type array */ public $elements = array( 'tr' => true, 'tbody' => true, 'thead' => true, 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true ); public function __construct() { } /** * @param array $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { if (empty($children)) { return false; } // only one of these elements is allowed in a table $caption = false; $thead = false; $tfoot = false; // whitespace $initial_ws = array(); $after_caption_ws = array(); $after_thead_ws = array(); $after_tfoot_ws = array(); // as many of these as you want $cols = array(); $content = array(); $tbody_mode = false; // if true, then we need to wrap any stray // <tr>s with a <tbody>. $ws_accum =& $initial_ws; foreach ($children as $node) { if ($node instanceof HTMLPurifier_Node_Comment) { $ws_accum[] = $node; continue; } switch ($node->name) { case 'tbody': $tbody_mode = true; // fall through case 'tr': $content[] = $node; $ws_accum =& $content; break; case 'caption': // there can only be one caption! if ($caption !== false) break; $caption = $node; $ws_accum =& $after_caption_ws; break; case 'thead': $tbody_mode = true; // XXX This breaks rendering properties with // Firefox, which never floats a <thead> to // the top. Ever. (Our scheme will float the // first <thead> to the top.) So maybe // <thead>s that are not first should be // turned into <tbody>? Very tricky, indeed. if ($thead === false) { $thead = $node; $ws_accum =& $after_thead_ws; } else { // Oops, there's a second one! What // should we do? Current behavior is to // transmutate the first and last entries into // tbody tags, and then put into content. // Maybe a better idea is to *attach // it* to the existing thead or tfoot? // We don't do this, because Firefox // doesn't float an extra tfoot to the // bottom like it does for the first one. $node->name = 'tbody'; $content[] = $node; $ws_accum =& $content; } break; case 'tfoot': // see above for some aveats $tbody_mode = true; if ($tfoot === false) { $tfoot = $node; $ws_accum =& $after_tfoot_ws; } else { $node->name = 'tbody'; $content[] = $node; $ws_accum =& $content; } break; case 'colgroup': case 'col': $cols[] = $node; $ws_accum =& $cols; break; case '#PCDATA': // How is whitespace handled? We treat is as sticky to // the *end* of the previous element. So all of the // nonsense we have worked on is to keep things // together. if (!empty($node->is_whitespace)) { $ws_accum[] = $node; } break; } } if (empty($content) && $thead === false && $tfoot === false) { return false; } $ret = $initial_ws; if ($caption !== false) { $ret[] = $caption; $ret = array_merge($ret, $after_caption_ws); } if ($cols !== false) { $ret = array_merge($ret, $cols); } if ($thead !== false) { $ret[] = $thead; $ret = array_merge($ret, $after_thead_ws); } if ($tfoot !== false) { $ret[] = $tfoot; $ret = array_merge($ret, $after_tfoot_ws); } if ($tbody_mode) { // we have to shuffle tr into tbody $current_tr_tbody = null; foreach($content as $node) { switch ($node->name) { case 'tbody': $current_tr_tbody = null; $ret[] = $node; break; case 'tr': if ($current_tr_tbody === null) { $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); $ret[] = $current_tr_tbody; } $current_tr_tbody->children[] = $node; break; case '#PCDATA': //assert($node->is_whitespace); if ($current_tr_tbody === null) { $ret[] = $node; } else { $current_tr_tbody->children[] = $node; } break; } } } else { $ret = array_merge($ret, $content); } return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Custom.php 0000644 00000005255 15111210767 0012453 0 ustar 00 <?php /** * Custom validation class, accepts DTD child definitions * * @warning Currently this class is an all or nothing proposition, that is, * it will only give a bool return value. */ class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef { /** * @type string */ public $type = 'custom'; /** * @type bool */ public $allow_empty = false; /** * Allowed child pattern as defined by the DTD. * @type string */ public $dtd_regex; /** * PCRE regex derived from $dtd_regex. * @type string */ private $_pcre_regex; /** * @param $dtd_regex Allowed child pattern from the DTD */ public function __construct($dtd_regex) { $this->dtd_regex = $dtd_regex; $this->_compileRegex(); } /** * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) */ protected function _compileRegex() { $raw = str_replace(' ', '', $this->dtd_regex); if ($raw[0] != '(') { $raw = "($raw)"; } $el = '[#a-zA-Z0-9_.-]+'; $reg = $raw; // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M // DOING! Seriously: if there's problems, please report them. // collect all elements into the $elements array preg_match_all("/$el/", $reg, $matches); foreach ($matches[0] as $match) { $this->elements[$match] = true; } // setup all elements as parentheticals with leading commas $reg = preg_replace("/$el/", '(,\\0)', $reg); // remove commas when they were not solicited $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); // remove all non-paranthetical commas: they are handled by first regex $reg = preg_replace("/,\(/", '(', $reg); $this->_pcre_regex = $reg; } /** * @param HTMLPurifier_Node[] $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function validateChildren($children, $config, $context) { $list_of_children = ''; $nesting = 0; // depth into the nest foreach ($children as $node) { if (!empty($node->is_whitespace)) { continue; } $list_of_children .= $node->name . ','; } // add leading comma to deal with stray comma declarations $list_of_children = ',' . rtrim($list_of_children, ','); $okay = preg_match( '/^,?' . $this->_pcre_regex . '$/', $list_of_children ); return (bool)$okay; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/List.php 0000644 00000005736 15111210767 0012120 0 ustar 00 <?php /** * Definition for list containers ul and ol. * * What does this do? The big thing is to handle ol/ul at the top * level of list nodes, which should be handled specially by /folding/ * them into the previous list node. We generally shouldn't ever * see other disallowed elements, because the autoclose behavior * in MakeWellFormed handles it. */ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef { /** * @type string */ public $type = 'list'; /** * @type array */ // lying a little bit, so that we can handle ul and ol ourselves // XXX: This whole business with 'wrap' is all a bit unsatisfactory public $elements = array('li' => true, 'ul' => true, 'ol' => true); public $whitespace; /** * @param array $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { // Flag for subclasses $this->whitespace = false; // if there are no tokens, delete parent node if (empty($children)) { return false; } // if li is not allowed, delete parent node if (!isset($config->getHTMLDefinition()->info['li'])) { trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); return false; } // the new set of children $result = array(); // a little sanity check to make sure it's not ALL whitespace $all_whitespace = true; $current_li = null; foreach ($children as $node) { if (!empty($node->is_whitespace)) { $result[] = $node; continue; } $all_whitespace = false; // phew, we're not talking about whitespace if ($node->name === 'li') { // good $current_li = $node; $result[] = $node; } else { // we want to tuck this into the previous li // Invariant: we expect the node to be ol/ul // ToDo: Make this more robust in the case of not ol/ul // by distinguishing between existing li and li created // to handle non-list elements; non-list elements should // not be appended to an existing li; only li created // for non-list. This distinction is not currently made. if ($current_li === null) { $current_li = new HTMLPurifier_Node_Element('li'); $result[] = $current_li; } $current_li->children[] = $node; $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo } } if (empty($result)) { return false; } if ($all_whitespace) { return false; } return $result; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Optional.php 0000644 00000002271 15111210770 0012753 0 ustar 00 <?php /** * Definition that allows a set of elements, and allows no children. * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required, * really, one shouldn't inherit from the other. Only altered behavior * is to overload a returned false with an array. Thus, it will never * return false. */ class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required { /** * @type bool */ public $allow_empty = true; /** * @type string */ public $type = 'optional'; /** * @param array $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { $result = parent::validateChildren($children, $config, $context); // we assume that $children is not modified if ($result === false) { if (empty($children)) { return true; } elseif ($this->whitespace) { return $children; } else { return array(); } } return $result; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Empty.php 0000644 00000001542 15111210770 0012264 0 ustar 00 <?php /** * Definition that disallows all elements. * @warning validateChildren() in this class is actually never called, because * empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed * before child definitions are parsed in earnest by * HTMLPurifier_Strategy_FixNesting. */ class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef { /** * @type bool */ public $allow_empty = true; /** * @type string */ public $type = 'empty'; public function __construct() { } /** * @param HTMLPurifier_Node[] $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { return array(); } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/StrictBlockquote.php 0000644 00000005542 15111210770 0014473 0 ustar 00 <?php /** * Takes the contents of blockquote when in strict and reformats for validation. */ class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required { /** * @type array */ protected $real_elements; /** * @type array */ protected $fake_elements; /** * @type bool */ public $allow_empty = true; /** * @type string */ public $type = 'strictblockquote'; /** * @type bool */ protected $init = false; /** * @param HTMLPurifier_Config $config * @return array * @note We don't want MakeWellFormed to auto-close inline elements since * they might be allowed. */ public function getAllowedElements($config) { $this->init($config); return $this->fake_elements; } /** * @param array $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { $this->init($config); // trick the parent class into thinking it allows more $this->elements = $this->fake_elements; $result = parent::validateChildren($children, $config, $context); $this->elements = $this->real_elements; if ($result === false) { return array(); } if ($result === true) { $result = $children; } $def = $config->getHTMLDefinition(); $block_wrap_name = $def->info_block_wrapper; $block_wrap = false; $ret = array(); foreach ($result as $node) { if ($block_wrap === false) { if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) || ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) { $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper); $ret[] = $block_wrap; } } else { if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) { $block_wrap = false; } } if ($block_wrap) { $block_wrap->children[] = $node; } else { $ret[] = $node; } } return $ret; } /** * @param HTMLPurifier_Config $config */ private function init($config) { if (!$this->init) { $def = $config->getHTMLDefinition(); // allow all inline elements $this->real_elements = $this->elements; $this->fake_elements = $def->info_content_sets['Flow']; $this->fake_elements['#PCDATA'] = true; $this->init = true; } } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Required.php 0000644 00000006426 15111210770 0012754 0 ustar 00 <?php /** * Definition that allows a set of elements, but disallows empty children. */ class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef { /** * Lookup table of allowed elements. * @type array */ public $elements = array(); /** * Whether or not the last passed node was all whitespace. * @type bool */ protected $whitespace = false; /** * @param array|string $elements List of allowed element names (lowercase). */ public function __construct($elements) { if (is_string($elements)) { $elements = str_replace(' ', '', $elements); $elements = explode('|', $elements); } $keys = array_keys($elements); if ($keys == array_keys($keys)) { $elements = array_flip($elements); foreach ($elements as $i => $x) { $elements[$i] = true; if (empty($i)) { unset($elements[$i]); } // remove blank } } $this->elements = $elements; } /** * @type bool */ public $allow_empty = false; /** * @type string */ public $type = 'required'; /** * @param array $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function validateChildren($children, $config, $context) { // Flag for subclasses $this->whitespace = false; // if there are no tokens, delete parent node if (empty($children)) { return false; } // the new set of children $result = array(); // whether or not parsed character data is allowed // this controls whether or not we silently drop a tag // or generate escaped HTML from it $pcdata_allowed = isset($this->elements['#PCDATA']); // a little sanity check to make sure it's not ALL whitespace $all_whitespace = true; $stack = array_reverse($children); while (!empty($stack)) { $node = array_pop($stack); if (!empty($node->is_whitespace)) { $result[] = $node; continue; } $all_whitespace = false; // phew, we're not talking about whitespace if (!isset($this->elements[$node->name])) { // special case text // XXX One of these ought to be redundant or something if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) { $result[] = $node; continue; } // spill the child contents in // ToDo: Make configurable if ($node instanceof HTMLPurifier_Node_Element) { for ($i = count($node->children) - 1; $i >= 0; $i--) { $stack[] = $node->children[$i]; } continue; } continue; } $result[] = $node; } if (empty($result)) { return false; } if ($all_whitespace) { $this->whitespace = true; return false; } return $result; } } // vim: et sw=4 sts=4 HTMLPurifier/ChildDef/Chameleon.php 0000644 00000003552 15111210770 0013064 0 ustar 00 <?php /** * Definition that uses different definitions depending on context. * * The del and ins tags are notable because they allow different types of * elements depending on whether or not they're in a block or inline context. * Chameleon allows this behavior to happen by using two different * definitions depending on context. While this somewhat generalized, * it is specifically intended for those two tags. */ class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef { /** * Instance of the definition object to use when inline. Usually stricter. * @type HTMLPurifier_ChildDef_Optional */ public $inline; /** * Instance of the definition object to use when block. * @type HTMLPurifier_ChildDef_Optional */ public $block; /** * @type string */ public $type = 'chameleon'; /** * @param array $inline List of elements to allow when inline. * @param array $block List of elements to allow when block. */ public function __construct($inline, $block) { $this->inline = new HTMLPurifier_ChildDef_Optional($inline); $this->block = new HTMLPurifier_ChildDef_Optional($block); $this->elements = $this->block->elements; } /** * @param HTMLPurifier_Node[] $children * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function validateChildren($children, $config, $context) { if ($context->get('IsInline') === false) { return $this->block->validateChildren( $children, $config, $context ); } else { return $this->inline->validateChildren( $children, $config, $context ); } } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/XHTML.php 0000644 00000000667 15111210770 0013252 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy { /** * @type string */ public $name = 'Tidy_XHTML'; /** * @type string */ public $defaultLevel = 'medium'; /** * @return array */ public function makeFixes() { $r = array(); $r['@lang'] = new HTMLPurifier_AttrTransform_Lang(); return $r; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/Name.php 0000644 00000001365 15111210770 0013232 0 ustar 00 <?php /** * Name is deprecated, but allowed in strict doctypes, so onl */ class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy { /** * @type string */ public $name = 'Tidy_Name'; /** * @type string */ public $defaultLevel = 'heavy'; /** * @return array */ public function makeFixes() { $r = array(); // @name for img, a ----------------------------------------------- // Technically, it's allowed even on strict, so we allow authors to use // it. However, it's deprecated in future versions of XHTML. $r['img@name'] = $r['a@name'] = new HTMLPurifier_AttrTransform_Name(); return $r; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/Transitional.php 0000644 00000000432 15111210770 0015013 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 { /** * @type string */ public $name = 'Tidy_Transitional'; /** * @type string */ public $defaultLevel = 'heavy'; } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php 0000644 00000015771 15111210770 0014450 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy { /** * @return array */ public function makeFixes() { $r = array(); // == deprecated tag transforms =================================== $r['font'] = new HTMLPurifier_TagTransform_Font(); $r['menu'] = new HTMLPurifier_TagTransform_Simple('ul'); $r['dir'] = new HTMLPurifier_TagTransform_Simple('ul'); $r['center'] = new HTMLPurifier_TagTransform_Simple('div', 'text-align:center;'); $r['u'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:underline;'); $r['s'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;'); $r['strike'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;'); // == deprecated attribute transforms ============================= $r['caption@align'] = new HTMLPurifier_AttrTransform_EnumToCSS( 'align', array( // we're following IE's behavior, not Firefox's, due // to the fact that no one supports caption-side:right, // W3C included (with CSS 2.1). This is a slightly // unreasonable attribute! 'left' => 'text-align:left;', 'right' => 'text-align:right;', 'top' => 'caption-side:top;', 'bottom' => 'caption-side:bottom;' // not supported by IE ) ); // @align for img ------------------------------------------------- $r['img@align'] = new HTMLPurifier_AttrTransform_EnumToCSS( 'align', array( 'left' => 'float:left;', 'right' => 'float:right;', 'top' => 'vertical-align:top;', 'middle' => 'vertical-align:middle;', 'bottom' => 'vertical-align:baseline;', ) ); // @align for table ----------------------------------------------- $r['table@align'] = new HTMLPurifier_AttrTransform_EnumToCSS( 'align', array( 'left' => 'float:left;', 'center' => 'margin-left:auto;margin-right:auto;', 'right' => 'float:right;' ) ); // @align for hr ----------------------------------------------- $r['hr@align'] = new HTMLPurifier_AttrTransform_EnumToCSS( 'align', array( // we use both text-align and margin because these work // for different browsers (IE and Firefox, respectively) // and the melange makes for a pretty cross-compatible // solution 'left' => 'margin-left:0;margin-right:auto;text-align:left;', 'center' => 'margin-left:auto;margin-right:auto;text-align:center;', 'right' => 'margin-left:auto;margin-right:0;text-align:right;' ) ); // @align for h1, h2, h3, h4, h5, h6, p, div ---------------------- // {{{ $align_lookup = array(); $align_values = array('left', 'right', 'center', 'justify'); foreach ($align_values as $v) { $align_lookup[$v] = "text-align:$v;"; } // }}} $r['h1@align'] = $r['h2@align'] = $r['h3@align'] = $r['h4@align'] = $r['h5@align'] = $r['h6@align'] = $r['p@align'] = $r['div@align'] = new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup); // @bgcolor for table, tr, td, th --------------------------------- $r['table@bgcolor'] = $r['tr@bgcolor'] = $r['td@bgcolor'] = $r['th@bgcolor'] = new HTMLPurifier_AttrTransform_BgColor(); // @border for img ------------------------------------------------ $r['img@border'] = new HTMLPurifier_AttrTransform_Border(); // @clear for br -------------------------------------------------- $r['br@clear'] = new HTMLPurifier_AttrTransform_EnumToCSS( 'clear', array( 'left' => 'clear:left;', 'right' => 'clear:right;', 'all' => 'clear:both;', 'none' => 'clear:none;', ) ); // @height for td, th --------------------------------------------- $r['td@height'] = $r['th@height'] = new HTMLPurifier_AttrTransform_Length('height'); // @hspace for img ------------------------------------------------ $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace'); // @noshade for hr ------------------------------------------------ // this transformation is not precise but often good enough. // different browsers use different styles to designate noshade $r['hr@noshade'] = new HTMLPurifier_AttrTransform_BoolToCSS( 'noshade', 'color:#808080;background-color:#808080;border:0;' ); // @nowrap for td, th --------------------------------------------- $r['td@nowrap'] = $r['th@nowrap'] = new HTMLPurifier_AttrTransform_BoolToCSS( 'nowrap', 'white-space:nowrap;' ); // @size for hr -------------------------------------------------- $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height'); // @type for li, ol, ul ------------------------------------------- // {{{ $ul_types = array( 'disc' => 'list-style-type:disc;', 'square' => 'list-style-type:square;', 'circle' => 'list-style-type:circle;' ); $ol_types = array( '1' => 'list-style-type:decimal;', 'i' => 'list-style-type:lower-roman;', 'I' => 'list-style-type:upper-roman;', 'a' => 'list-style-type:lower-alpha;', 'A' => 'list-style-type:upper-alpha;' ); $li_types = $ul_types + $ol_types; // }}} $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types); $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true); $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true); // @vspace for img ------------------------------------------------ $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace'); // @width for table, hr, td, th, col ------------------------------------------ $r['table@width'] = $r['td@width'] = $r['th@width'] = $r['col@width'] = $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width'); return $r; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/Proprietary.php 0000644 00000001772 15111210770 0014674 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy { /** * @type string */ public $name = 'Tidy_Proprietary'; /** * @type string */ public $defaultLevel = 'light'; /** * @return array */ public function makeFixes() { $r = array(); $r['table@background'] = new HTMLPurifier_AttrTransform_Background(); $r['td@background'] = new HTMLPurifier_AttrTransform_Background(); $r['th@background'] = new HTMLPurifier_AttrTransform_Background(); $r['tr@background'] = new HTMLPurifier_AttrTransform_Background(); $r['thead@background'] = new HTMLPurifier_AttrTransform_Background(); $r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background(); $r['tbody@background'] = new HTMLPurifier_AttrTransform_Background(); $r['table@height'] = new HTMLPurifier_AttrTransform_Length('height'); return $r; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy/Strict.php 0000644 00000001612 15111210770 0013615 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 { /** * @type string */ public $name = 'Tidy_Strict'; /** * @type string */ public $defaultLevel = 'light'; /** * @return array */ public function makeFixes() { $r = parent::makeFixes(); $r['blockquote#content_model_type'] = 'strictblockquote'; return $r; } /** * @type bool */ public $defines_child_def = true; /** * @param HTMLPurifier_ElementDef $def * @return HTMLPurifier_ChildDef_StrictBlockquote */ public function getChildDef($def) { if ($def->content_model_type != 'strictblockquote') { return parent::getChildDef($def); } return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/StyleAttribute.php 0000644 00000001414 15111210770 0014420 0 ustar 00 <?php /** * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension * Module. */ class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'StyleAttribute'; /** * @type array */ public $attr_collections = array( // The inclusion routine differs from the Abstract Modules but // is in line with the DTD and XML Schemas. 'Style' => array('style' => false), // see constructor 'Core' => array(0 => array('Style')) ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/List.php 0000644 00000003565 15111210770 0012360 0 ustar 00 <?php /** * XHTML 1.1 List Module, defines list-oriented elements. Core Module. */ class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'List'; // According to the abstract schema, the List content set is a fully formed // one or more expr, but it invariably occurs in an optional declaration // so we're not going to do that subtlety. It might cause trouble // if a user defines "List" and expects that multiple lists are // allowed to be specified, but then again, that's not very intuitive. // Furthermore, the actual XML Schema may disagree. Regardless, // we don't have support for such nested expressions without using // the incredibly inefficient and draconic Custom ChildDef. /** * @type array */ public $content_sets = array('Flow' => 'List'); /** * @param HTMLPurifier_Config $config */ public function setup($config) { $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); // XXX The wrap attribute is handled by MakeWellFormed. This is all // quite unsatisfactory, because we generated this // *specifically* for lists, and now a big chunk of the handling // is done properly by the List ChildDef. So actually, we just // want enough information to make autoclosing work properly, // and then hand off the tricky stuff to the ChildDef. $ol->wrap = 'li'; $ul->wrap = 'li'; $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); $this->addElement('li', false, 'Flow', 'Common'); $this->addElement('dd', false, 'Flow', 'Common'); $this->addElement('dt', false, 'Inline', 'Common'); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Iframe.php 0000644 00000002231 15111210771 0012636 0 ustar 00 <?php /** * XHTML 1.1 Iframe Module provides inline frames. * * @note This module is not considered safe unless an Iframe * whitelisting mechanism is specified. Currently, the only * such mechanism is %URL.SafeIframeRegexp */ class HTMLPurifier_HTMLModule_Iframe extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Iframe'; /** * @type bool */ public $safe = false; /** * @param HTMLPurifier_Config $config */ public function setup($config) { if ($config->get('HTML.SafeIframe')) { $this->safe = true; } $this->addElement( 'iframe', 'Inline', 'Flow', 'Common', array( 'src' => 'URI#embedded', 'width' => 'Length', 'height' => 'Length', 'name' => 'ID', 'scrolling' => 'Enum#yes,no,auto', 'frameborder' => 'Enum#0,1', 'longdesc' => 'URI', 'marginheight' => 'Pixels', 'marginwidth' => 'Pixels', ) ); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Scripting.php 0000644 00000004441 15111210771 0013402 0 ustar 00 <?php /* WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!! */ /** * XHTML 1.1 Scripting module, defines elements that are used to contain * information pertaining to executable scripts or the lack of support * for executable scripts. * @note This module does not contain inline scripting elements */ class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Scripting'; /** * @type array */ public $elements = array('script', 'noscript'); /** * @type array */ public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript'); /** * @type bool */ public $safe = false; /** * @param HTMLPurifier_Config $config */ public function setup($config) { // TODO: create custom child-definition for noscript that // auto-wraps stray #PCDATA in a similar manner to // blockquote's custom definition (we would use it but // blockquote's contents are optional while noscript's contents // are required) // TODO: convert this to new syntax, main problem is getting // both content sets working // In theory, this could be safe, but I don't see any reason to // allow it. $this->info['noscript'] = new HTMLPurifier_ElementDef(); $this->info['noscript']->attr = array(0 => array('Common')); $this->info['noscript']->content_model = 'Heading | List | Block'; $this->info['noscript']->content_model_type = 'required'; $this->info['script'] = new HTMLPurifier_ElementDef(); $this->info['script']->attr = array( 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')), 'src' => new HTMLPurifier_AttrDef_URI(true), 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript')) ); $this->info['script']->content_model = '#PCDATA'; $this->info['script']->content_model_type = 'optional'; $this->info['script']->attr_transform_pre[] = $this->info['script']->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/SafeObject.php 0000644 00000003633 15111210771 0013447 0 ustar 00 <?php /** * A "safe" object module. In theory, objects permitted by this module will * be safe, and untrusted users can be allowed to embed arbitrary flash objects * (maybe other types too, but only Flash is supported as of right now). * Highly experimental. */ class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'SafeObject'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { // These definitions are not intrinsically safe: the attribute transforms // are a vital part of ensuring safety. $max = $config->get('HTML.MaxImgLength'); $object = $this->addElement( 'object', 'Inline', 'Optional: param | Flow | #PCDATA', 'Common', array( // While technically not required by the spec, we're forcing // it to this value. 'type' => 'Enum#application/x-shockwave-flash', 'width' => 'Pixels#' . $max, 'height' => 'Pixels#' . $max, 'data' => 'URI#embedded', 'codebase' => new HTMLPurifier_AttrDef_Enum( array( 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' ) ), ) ); $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); $param = $this->addElement( 'param', false, 'Empty', false, array( 'id' => 'ID', 'name*' => 'Text', 'value' => 'Text' ) ); $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); $this->info_injector[] = 'SafeObject'; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/TargetNoopener.php 0000644 00000001004 15111210771 0014364 0 ustar 00 <?php /** * Module adds the target-based noopener attribute transformation to a tags. It * is enabled by HTML.TargetNoopener */ class HTMLPurifier_HTMLModule_TargetNoopener extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'TargetNoopener'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $a = $this->addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener(); } } HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php 0000644 00000000542 15111210771 0015771 0 ustar 00 <?php class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'NonXMLCommonAttributes'; /** * @type array */ public $attr_collections = array( 'Lang' => array( 'lang' => 'LanguageCode', ) ); } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Name.php 0000644 00000001235 15111210771 0012316 0 ustar 00 <?php class HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Name'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map'); foreach ($elements as $name) { $element = $this->addBlankElement($name); $element->attr['name'] = 'CDATA'; if (!$config->get('HTML.Attr.Name.UseCDATA')) { $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); } } } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Bdo.php 0000644 00000002004 15111210771 0012135 0 ustar 00 <?php /** * XHTML 1.1 Bi-directional Text Module, defines elements that * declare directionality of content. Text Extension Module. */ class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Bdo'; /** * @type array */ public $attr_collections = array( 'I18N' => array('dir' => false) ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { $bdo = $this->addElement( 'bdo', 'Inline', 'Inline', array('Core', 'Lang'), array( 'dir' => 'Enum#ltr,rtl', // required // The Abstract Module specification has the attribute // inclusions wrong for bdo: bdo allows Lang ) ); $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Nofollow.php 0000644 00000000773 15111210771 0013243 0 ustar 00 <?php /** * Module adds the nofollow attribute transformation to a tags. It * is enabled by HTML.Nofollow */ class HTMLPurifier_HTMLModule_Nofollow extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Nofollow'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $a = $this->addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Presentation.php 0000644 00000002607 15111210771 0014115 0 ustar 00 <?php /** * XHTML 1.1 Presentation Module, defines simple presentation-related * markup. Text Extension Module. * @note The official XML Schema and DTD specs further divide this into * two modules: * - Block Presentation (hr) * - Inline Presentation (b, big, i, small, sub, sup, tt) * We have chosen not to heed this distinction, as content_sets * provides satisfactory disambiguation. */ class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Presentation'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement('hr', 'Block', 'Empty', 'Common'); $this->addElement('sub', 'Inline', 'Inline', 'Common'); $this->addElement('sup', 'Inline', 'Inline', 'Common'); $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); $b->formatting = true; $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); $big->formatting = true; $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); $i->formatting = true; $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); $small->formatting = true; $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); $tt->formatting = true; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Edit.php 0000644 00000002632 15111210771 0012325 0 ustar 00 <?php /** * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension * Module. */ class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Edit'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow'; $attr = array( 'cite' => 'URI', // 'datetime' => 'Datetime', // not implemented ); $this->addElement('del', 'Inline', $contents, 'Common', $attr); $this->addElement('ins', 'Inline', $contents, 'Common', $attr); } // HTML 4.01 specifies that ins/del must not contain block // elements when used in an inline context, chameleon is // a complicated workaround to acheive this effect // Inline context ! Block context (exclamation mark is // separator, see getChildDef for parsing) /** * @type bool */ public $defines_child_def = true; /** * @param HTMLPurifier_ElementDef $def * @return HTMLPurifier_ChildDef_Chameleon */ public function getChildDef($def) { if ($def->content_model_type != 'chameleon') { return false; } $value = explode('!', $def->content_model); return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/SafeScripting.php 0000644 00000002357 15111210771 0014205 0 ustar 00 <?php /** * A "safe" script module. No inline JS is allowed, and pointed to JS * files must match whitelist. */ class HTMLPurifier_HTMLModule_SafeScripting extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'SafeScripting'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { // These definitions are not intrinsically safe: the attribute transforms // are a vital part of ensuring safety. $allowed = $config->get('HTML.SafeScripting'); $script = $this->addElement( 'script', 'Inline', 'Optional:', // Not `Empty` to not allow to autoclose the <script /> tag @see https://www.w3.org/TR/html4/interact/scripts.html null, array( // While technically not required by the spec, we're forcing // it to this value. 'type' => 'Enum#text/javascript', 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed), /*case sensitive*/ true) ) ); $script->attr_transform_pre[] = $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/SafeEmbed.php 0000644 00000002111 15111210771 0013243 0 ustar 00 <?php /** * A "safe" embed module. See SafeObject. This is a proprietary element. */ class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'SafeEmbed'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $max = $config->get('HTML.MaxImgLength'); $embed = $this->addElement( 'embed', 'Inline', 'Empty', 'Common', array( 'src*' => 'URI#embedded', 'type' => 'Enum#application/x-shockwave-flash', 'width' => 'Pixels#' . $max, 'height' => 'Pixels#' . $max, 'allowscriptaccess' => 'Enum#never', 'allownetworking' => 'Enum#internal', 'flashvars' => 'Text', 'wmode' => 'Enum#window,transparent,opaque', 'name' => 'ID', ) ); $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/TargetNoreferrer.php 0000644 00000001016 15111210771 0014713 0 ustar 00 <?php /** * Module adds the target-based noreferrer attribute transformation to a tags. It * is enabled by HTML.TargetNoreferrer */ class HTMLPurifier_HTMLModule_TargetNoreferrer extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'TargetNoreferrer'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $a = $this->addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoreferrer(); } } HTMLPurifier/HTMLModule/Proprietary.php 0000644 00000001743 15111210771 0013762 0 ustar 00 <?php /** * Module defines proprietary tags and attributes in HTML. * @warning If this module is enabled, standards-compliance is off! */ class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Proprietary'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement( 'marquee', 'Inline', 'Flow', 'Common', array( 'direction' => 'Enum#left,right,up,down', 'behavior' => 'Enum#alternate', 'width' => 'Length', 'height' => 'Length', 'scrolldelay' => 'Number', 'scrollamount' => 'Number', 'loop' => 'Number', 'bgcolor' => 'Color', 'hspace' => 'Pixels', 'vspace' => 'Pixels', ) ); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/CommonAttributes.php 0000644 00000001322 15111210771 0014732 0 ustar 00 <?php class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'CommonAttributes'; /** * @type array */ public $attr_collections = array( 'Core' => array( 0 => array('Style'), // 'xml:space' => false, 'class' => 'Class', 'id' => 'ID', 'title' => 'CDATA', 'contenteditable' => 'ContentEditable', ), 'Lang' => array(), 'I18N' => array( 0 => array('Lang'), // proprietary, for xml:lang/lang ), 'Common' => array( 0 => array('Core', 'I18N') ) ); } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Target.php 0000644 00000001127 15111210772 0012665 0 ustar 00 <?php /** * XHTML 1.1 Target Module, defines target attribute in link elements. */ class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Target'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $elements = array('a'); foreach ($elements as $name) { $e = $this->addBlankElement($name); $e->attr = array( 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget() ); } } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Hypertext.php 0000644 00000001744 15111210772 0013440 0 ustar 00 <?php /** * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module. */ class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Hypertext'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $a = $this->addElement( 'a', 'Inline', 'Inline', 'Common', array( // 'accesskey' => 'Character', // 'charset' => 'Charset', 'href' => 'URI', // 'hreflang' => 'LanguageCode', 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), // 'tabindex' => 'Number', // 'type' => 'ContentType', ) ); $a->formatting = true; $a->excludes = array('a' => true); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Object.php 0000644 00000002763 15111210772 0012654 0 ustar 00 <?php /** * XHTML 1.1 Object Module, defines elements for generic object inclusion * @warning Users will commonly use <embed> to cater to legacy browsers: this * module does not allow this sort of behavior */ class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Object'; /** * @type bool */ public $safe = false; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement( 'object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common', array( 'archive' => 'URI', 'classid' => 'URI', 'codebase' => 'URI', 'codetype' => 'Text', 'data' => 'URI', 'declare' => 'Bool#declare', 'height' => 'Length', 'name' => 'CDATA', 'standby' => 'Text', 'tabindex' => 'Number', 'type' => 'ContentType', 'width' => 'Length' ) ); $this->addElement( 'param', false, 'Empty', null, array( 'id' => 'ID', 'name*' => 'Text', 'type' => 'Text', 'value' => 'Text', 'valuetype' => 'Enum#data,ref,object' ) ); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Text.php 0000644 00000006553 15111210772 0012373 0 ustar 00 <?php /** * XHTML 1.1 Text Module, defines basic text containers. Core Module. * @note In the normative XML Schema specification, this module * is further abstracted into the following modules: * - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6) * - Block Structural (div, p) * - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var) * - Inline Structural (br, span) * This module, functionally, does not distinguish between these * sub-modules, but the code is internally structured to reflect * these distinctions. */ class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Text'; /** * @type array */ public $content_sets = array( 'Flow' => 'Heading | Block | Inline' ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { // Inline Phrasal ------------------------------------------------- $this->addElement('abbr', 'Inline', 'Inline', 'Common'); $this->addElement('acronym', 'Inline', 'Inline', 'Common'); $this->addElement('cite', 'Inline', 'Inline', 'Common'); $this->addElement('dfn', 'Inline', 'Inline', 'Common'); $this->addElement('kbd', 'Inline', 'Inline', 'Common'); $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI')); $this->addElement('samp', 'Inline', 'Inline', 'Common'); $this->addElement('var', 'Inline', 'Inline', 'Common'); $em = $this->addElement('em', 'Inline', 'Inline', 'Common'); $em->formatting = true; $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common'); $strong->formatting = true; $code = $this->addElement('code', 'Inline', 'Inline', 'Common'); $code->formatting = true; // Inline Structural ---------------------------------------------- $this->addElement('span', 'Inline', 'Inline', 'Common'); $this->addElement('br', 'Inline', 'Empty', 'Core'); // Block Phrasal -------------------------------------------------- $this->addElement('address', 'Block', 'Inline', 'Common'); $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI')); $pre = $this->addElement('pre', 'Block', 'Inline', 'Common'); $pre->excludes = $this->makeLookup( 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' ); $this->addElement('h1', 'Heading', 'Inline', 'Common'); $this->addElement('h2', 'Heading', 'Inline', 'Common'); $this->addElement('h3', 'Heading', 'Inline', 'Common'); $this->addElement('h4', 'Heading', 'Inline', 'Common'); $this->addElement('h5', 'Heading', 'Inline', 'Common'); $this->addElement('h6', 'Heading', 'Inline', 'Common'); // Block Structural ----------------------------------------------- $p = $this->addElement('p', 'Block', 'Inline', 'Common'); $p->autoclose = array_flip( array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul") ); $this->addElement('div', 'Block', 'Flow', 'Common'); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tidy.php 0000644 00000016067 15111210772 0012361 0 ustar 00 <?php /** * Abstract class for a set of proprietary modules that clean up (tidy) * poorly written HTML. * @todo Figure out how to protect some of these methods/properties */ class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule { /** * List of supported levels. * Index zero is a special case "no fixes" level. * @type array */ public $levels = array(0 => 'none', 'light', 'medium', 'heavy'); /** * Default level to place all fixes in. * Disabled by default. * @type string */ public $defaultLevel = null; /** * Lists of fixes used by getFixesForLevel(). * Format is: * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2'); * @type array */ public $fixesForLevel = array( 'light' => array(), 'medium' => array(), 'heavy' => array() ); /** * Lazy load constructs the module by determining the necessary * fixes to create and then delegating to the populate() function. * @param HTMLPurifier_Config $config * @todo Wildcard matching and error reporting when an added or * subtracted fix has no effect. */ public function setup($config) { // create fixes, initialize fixesForLevel $fixes = $this->makeFixes(); $this->makeFixesForLevel($fixes); // figure out which fixes to use $level = $config->get('HTML.TidyLevel'); $fixes_lookup = $this->getFixesForLevel($level); // get custom fix declarations: these need namespace processing $add_fixes = $config->get('HTML.TidyAdd'); $remove_fixes = $config->get('HTML.TidyRemove'); foreach ($fixes as $name => $fix) { // needs to be refactored a little to implement globbing if (isset($remove_fixes[$name]) || (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))) { unset($fixes[$name]); } } // populate this module with necessary fixes $this->populate($fixes); } /** * Retrieves all fixes per a level, returning fixes for that specific * level as well as all levels below it. * @param string $level level identifier, see $levels for valid values * @return array Lookup up table of fixes */ public function getFixesForLevel($level) { if ($level == $this->levels[0]) { return array(); } $activated_levels = array(); for ($i = 1, $c = count($this->levels); $i < $c; $i++) { $activated_levels[] = $this->levels[$i]; if ($this->levels[$i] == $level) { break; } } if ($i == $c) { trigger_error( 'Tidy level ' . htmlspecialchars($level) . ' not recognized', E_USER_WARNING ); return array(); } $ret = array(); foreach ($activated_levels as $level) { foreach ($this->fixesForLevel[$level] as $fix) { $ret[$fix] = true; } } return $ret; } /** * Dynamically populates the $fixesForLevel member variable using * the fixes array. It may be custom overloaded, used in conjunction * with $defaultLevel, or not used at all. * @param array $fixes */ public function makeFixesForLevel($fixes) { if (!isset($this->defaultLevel)) { return; } if (!isset($this->fixesForLevel[$this->defaultLevel])) { trigger_error( 'Default level ' . $this->defaultLevel . ' does not exist', E_USER_ERROR ); return; } $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes); } /** * Populates the module with transforms and other special-case code * based on a list of fixes passed to it * @param array $fixes Lookup table of fixes to activate */ public function populate($fixes) { foreach ($fixes as $name => $fix) { // determine what the fix is for list($type, $params) = $this->getFixType($name); switch ($type) { case 'attr_transform_pre': case 'attr_transform_post': $attr = $params['attr']; if (isset($params['element'])) { $element = $params['element']; if (empty($this->info[$element])) { $e = $this->addBlankElement($element); } else { $e = $this->info[$element]; } } else { $type = "info_$type"; $e = $this; } $e->{$type}[$attr] = $fix; break; case 'tag_transform': $this->info_tag_transform[$params['element']] = $fix; break; case 'child': case 'content_model_type': $element = $params['element']; if (empty($this->info[$element])) { $e = $this->addBlankElement($element); } else { $e = $this->info[$element]; } $e->$type = $fix; break; default: trigger_error("Fix type $type not supported", E_USER_ERROR); break; } } } /** * Parses a fix name and determines what kind of fix it is, as well * as other information defined by the fix * @param $name String name of fix * @return array(string $fix_type, array $fix_parameters) * @note $fix_parameters is type dependant, see populate() for usage * of these parameters */ public function getFixType($name) { // parse it $property = $attr = null; if (strpos($name, '#') !== false) { list($name, $property) = explode('#', $name); } if (strpos($name, '@') !== false) { list($name, $attr) = explode('@', $name); } // figure out the parameters $params = array(); if ($name !== '') { $params['element'] = $name; } if (!is_null($attr)) { $params['attr'] = $attr; } // special case: attribute transform if (!is_null($attr)) { if (is_null($property)) { $property = 'pre'; } $type = 'attr_transform_' . $property; return array($type, $params); } // special case: tag transform if (is_null($property)) { return array('tag_transform', $params); } return array($property, $params); } /** * Defines all fixes the module will perform in a compact * associative array of fix name to fix implementation. * @return array */ public function makeFixes() { } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Image.php 0000644 00000002554 15111210772 0012466 0 ustar 00 <?php /** * XHTML 1.1 Image Module provides basic image embedding. * @note There is specialized code for removing empty images in * HTMLPurifier_Strategy_RemoveForeignElements */ class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Image'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $max = $config->get('HTML.MaxImgLength'); $img = $this->addElement( 'img', 'Inline', 'Empty', 'Common', array( 'alt*' => 'Text', // According to the spec, it's Length, but percents can // be abused, so we allow only Pixels. 'height' => 'Pixels#' . $max, 'width' => 'Pixels#' . $max, 'longdesc' => 'URI', 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded ) ); if ($max === null || $config->get('HTML.Trusted')) { $img->attr['height'] = $img->attr['width'] = 'Length'; } // kind of strange, but splitting things up would be inefficient $img->attr_transform_pre[] = $img->attr_transform_post[] = new HTMLPurifier_AttrTransform_ImgRequired(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Legacy.php 0000644 00000013337 15111210772 0012651 0 ustar 00 <?php /** * XHTML 1.1 Legacy module defines elements that were previously * deprecated. * * @note Not all legacy elements have been implemented yet, which * is a bit of a reverse problem as compared to browsers! In * addition, this legacy module may implement a bit more than * mandated by XHTML 1.1. * * This module can be used in combination with TransformToStrict in order * to transform as many deprecated elements as possible, but retain * questionably deprecated elements that do not have good alternatives * as well as transform elements that don't have an implementation. * See docs/ref-strictness.txt for more details. */ class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Legacy'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement( 'basefont', 'Inline', 'Empty', null, array( 'color' => 'Color', 'face' => 'Text', // extremely broad, we should 'size' => 'Text', // tighten it 'id' => 'ID' ) ); $this->addElement('center', 'Block', 'Flow', 'Common'); $this->addElement( 'dir', 'Block', 'Required: li', 'Common', array( 'compact' => 'Bool#compact' ) ); $this->addElement( 'font', 'Inline', 'Inline', array('Core', 'I18N'), array( 'color' => 'Color', 'face' => 'Text', // extremely broad, we should 'size' => 'Text', // tighten it ) ); $this->addElement( 'menu', 'Block', 'Required: li', 'Common', array( 'compact' => 'Bool#compact' ) ); $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); $s->formatting = true; $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); $strike->formatting = true; $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); $u->formatting = true; // setup modifications to old elements $align = 'Enum#left,right,center,justify'; $address = $this->addBlankElement('address'); $address->content_model = 'Inline | #PCDATA | p'; $address->content_model_type = 'optional'; $address->child = false; $blockquote = $this->addBlankElement('blockquote'); $blockquote->content_model = 'Flow | #PCDATA'; $blockquote->content_model_type = 'optional'; $blockquote->child = false; $br = $this->addBlankElement('br'); $br->attr['clear'] = 'Enum#left,all,right,none'; $caption = $this->addBlankElement('caption'); $caption->attr['align'] = 'Enum#top,bottom,left,right'; $div = $this->addBlankElement('div'); $div->attr['align'] = $align; $dl = $this->addBlankElement('dl'); $dl->attr['compact'] = 'Bool#compact'; for ($i = 1; $i <= 6; $i++) { $h = $this->addBlankElement("h$i"); $h->attr['align'] = $align; } $hr = $this->addBlankElement('hr'); $hr->attr['align'] = $align; $hr->attr['noshade'] = 'Bool#noshade'; $hr->attr['size'] = 'Pixels'; $hr->attr['width'] = 'Length'; $img = $this->addBlankElement('img'); $img->attr['align'] = 'IAlign'; $img->attr['border'] = 'Pixels'; $img->attr['hspace'] = 'Pixels'; $img->attr['vspace'] = 'Pixels'; // figure out this integer business $li = $this->addBlankElement('li'); $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; $ol = $this->addBlankElement('ol'); $ol->attr['compact'] = 'Bool#compact'; $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; $p = $this->addBlankElement('p'); $p->attr['align'] = $align; $pre = $this->addBlankElement('pre'); $pre->attr['width'] = 'Number'; // script omitted $table = $this->addBlankElement('table'); $table->attr['align'] = 'Enum#left,center,right'; $table->attr['bgcolor'] = 'Color'; $tr = $this->addBlankElement('tr'); $tr->attr['bgcolor'] = 'Color'; $th = $this->addBlankElement('th'); $th->attr['bgcolor'] = 'Color'; $th->attr['height'] = 'Length'; $th->attr['nowrap'] = 'Bool#nowrap'; $th->attr['width'] = 'Length'; $td = $this->addBlankElement('td'); $td->attr['bgcolor'] = 'Color'; $td->attr['height'] = 'Length'; $td->attr['nowrap'] = 'Bool#nowrap'; $td->attr['width'] = 'Length'; $ul = $this->addBlankElement('ul'); $ul->attr['compact'] = 'Bool#compact'; $ul->attr['type'] = 'Enum#square,disc,circle'; // "safe" modifications to "unsafe" elements // WARNING: If you want to add support for an unsafe, legacy // attribute, make a new TrustedLegacy module with the trusted // bit set appropriately $form = $this->addBlankElement('form'); $form->content_model = 'Flow | #PCDATA'; $form->content_model_type = 'optional'; $form->attr['target'] = 'FrameTarget'; $input = $this->addBlankElement('input'); $input->attr['align'] = 'IAlign'; $legend = $this->addBlankElement('legend'); $legend->attr['align'] = 'LAlign'; } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/XMLCommonAttributes.php 0000644 00000000540 15111210772 0015315 0 ustar 00 <?php class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'XMLCommonAttributes'; /** * @type array */ public $attr_collections = array( 'Lang' => array( 'xml:lang' => 'LanguageCode', ) ); } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Forms.php 0000644 00000013314 15111210772 0012526 0 ustar 00 <?php /** * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4. */ class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Forms'; /** * @type bool */ public $safe = false; /** * @type array */ public $content_sets = array( 'Block' => 'Form', 'Inline' => 'Formctrl', ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { if ($config->get('HTML.Forms')) { $this->safe = true; } $form = $this->addElement( 'form', 'Form', 'Required: Heading | List | Block | fieldset', 'Common', array( 'accept' => 'ContentTypes', 'accept-charset' => 'Charsets', 'action*' => 'URI', 'method' => 'Enum#get,post', // really ContentType, but these two are the only ones used today 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', ) ); $form->excludes = array('form' => true); $input = $this->addElement( 'input', 'Formctrl', 'Empty', 'Common', array( 'accept' => 'ContentTypes', 'accesskey' => 'Character', 'alt' => 'Text', 'checked' => 'Bool#checked', 'disabled' => 'Bool#disabled', 'maxlength' => 'Number', 'name' => 'CDATA', 'readonly' => 'Bool#readonly', 'size' => 'Number', 'src' => 'URI#embedded', 'tabindex' => 'Number', 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', 'value' => 'CDATA', ) ); $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); $this->addElement( 'select', 'Formctrl', 'Required: optgroup | option', 'Common', array( 'disabled' => 'Bool#disabled', 'multiple' => 'Bool#multiple', 'name' => 'CDATA', 'size' => 'Number', 'tabindex' => 'Number', ) ); $this->addElement( 'option', false, 'Optional: #PCDATA', 'Common', array( 'disabled' => 'Bool#disabled', 'label' => 'Text', 'selected' => 'Bool#selected', 'value' => 'CDATA', ) ); // It's illegal for there to be more than one selected, but not // be multiple. Also, no selected means undefined behavior. This might // be difficult to implement; perhaps an injector, or a context variable. $textarea = $this->addElement( 'textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array( 'accesskey' => 'Character', 'cols*' => 'Number', 'disabled' => 'Bool#disabled', 'name' => 'CDATA', 'readonly' => 'Bool#readonly', 'rows*' => 'Number', 'tabindex' => 'Number', ) ); $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); $button = $this->addElement( 'button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array( 'accesskey' => 'Character', 'disabled' => 'Bool#disabled', 'name' => 'CDATA', 'tabindex' => 'Number', 'type' => 'Enum#button,submit,reset', 'value' => 'CDATA', ) ); // For exclusions, ideally we'd specify content sets, not literal elements $button->excludes = $this->makeLookup( 'form', 'fieldset', // Form 'input', 'select', 'textarea', 'label', 'button', // Formctrl 'a', // as per HTML 4.01 spec, this is omitted by modularization 'isindex', 'iframe' // legacy items ); // Extra exclusion: img usemap="" is not permitted within this element. // We'll omit this for now, since we don't have any good way of // indicating it yet. // This is HIGHLY user-unfriendly; we need a custom child-def for this $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); $label = $this->addElement( 'label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array( 'accesskey' => 'Character', // 'for' => 'IDREF', // IDREF not implemented, cannot allow ) ); $label->excludes = array('label' => true); $this->addElement( 'legend', false, 'Optional: #PCDATA | Inline', 'Common', array( 'accesskey' => 'Character', ) ); $this->addElement( 'optgroup', false, 'Required: option', 'Common', array( 'disabled' => 'Bool#disabled', 'label*' => 'Text', ) ); // Don't forget an injector for <isindex>. This one's a little complex // because it maps to multiple elements. } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/TargetBlank.php 0000644 00000001012 15111210772 0013626 0 ustar 00 <?php /** * Module adds the target=blank attribute transformation to a tags. It * is enabled by HTML.TargetBlank */ class HTMLPurifier_HTMLModule_TargetBlank extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'TargetBlank'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $a = $this->addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank(); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Ruby.php 0000644 00000002036 15111210772 0012360 0 ustar 00 <?php /** * XHTML 1.1 Ruby Annotation Module, defines elements that indicate * short runs of text alongside base text for annotation or pronounciation. */ class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Ruby'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement( 'ruby', 'Inline', 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', 'Common' ); $this->addElement('rbc', false, 'Required: rb', 'Common'); $this->addElement('rtc', false, 'Required: rt', 'Common'); $rb = $this->addElement('rb', false, 'Inline', 'Common'); $rb->excludes = array('ruby' => true); $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); $rt->excludes = array('ruby' => true); $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); } } // vim: et sw=4 sts=4 HTMLPurifier/HTMLModule/Tables.php 0000644 00000004460 15111210772 0012654 0 ustar 00 <?php /** * XHTML 1.1 Tables Module, fully defines accessible table elements. */ class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Tables'; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement('caption', false, 'Inline', 'Common'); $this->addElement( 'table', 'Block', new HTMLPurifier_ChildDef_Table(), 'Common', array( 'border' => 'Pixels', 'cellpadding' => 'Length', 'cellspacing' => 'Length', 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border', 'rules' => 'Enum#none,groups,rows,cols,all', 'summary' => 'Text', 'width' => 'Length' ) ); // common attributes $cell_align = array( 'align' => 'Enum#left,center,right,justify,char', 'charoff' => 'Length', 'valign' => 'Enum#top,middle,bottom,baseline', ); $cell_t = array_merge( array( 'abbr' => 'Text', 'colspan' => 'Number', 'rowspan' => 'Number', // Apparently, as of HTML5 this attribute only applies // to 'th' elements. 'scope' => 'Enum#row,col,rowgroup,colgroup', ), $cell_align ); $this->addElement('td', false, 'Flow', 'Common', $cell_t); $this->addElement('th', false, 'Flow', 'Common', $cell_t); $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align); $cell_col = array_merge( array( 'span' => 'Number', 'width' => 'MultiLength', ), $cell_align ); $this->addElement('col', false, 'Empty', 'Common', $cell_col); $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col); $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align); $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align); $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align); } } // vim: et sw=4 sts=4 HTMLPurifier/URIDefinition.php 0000644 00000006546 15111210772 0012207 0 ustar 00 <?php class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition { public $type = 'URI'; protected $filters = array(); protected $postFilters = array(); protected $registeredFilters = array(); /** * HTMLPurifier_URI object of the base specified at %URI.Base */ public $base; /** * String host to consider "home" base, derived off of $base */ public $host; /** * Name of default scheme based on %URI.DefaultScheme and %URI.Base */ public $defaultScheme; public function __construct() { $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal()); $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); $this->registerFilter(new HTMLPurifier_URIFilter_DisableResources()); $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); $this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe()); $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); } public function registerFilter($filter) { $this->registeredFilters[$filter->name] = $filter; } public function addFilter($filter, $config) { $r = $filter->prepare($config); if ($r === false) return; // null is ok, for backwards compat if ($filter->post) { $this->postFilters[$filter->name] = $filter; } else { $this->filters[$filter->name] = $filter; } } protected function doSetup($config) { $this->setupMemberVariables($config); $this->setupFilters($config); } protected function setupFilters($config) { foreach ($this->registeredFilters as $name => $filter) { if ($filter->always_load) { $this->addFilter($filter, $config); } else { $conf = $config->get('URI.' . $name); if ($conf !== false && $conf !== null) { $this->addFilter($filter, $config); } } } unset($this->registeredFilters); } protected function setupMemberVariables($config) { $this->host = $config->get('URI.Host'); $base_uri = $config->get('URI.Base'); if (!is_null($base_uri)) { $parser = new HTMLPurifier_URIParser(); $this->base = $parser->parse($base_uri); $this->defaultScheme = $this->base->scheme; if (is_null($this->host)) $this->host = $this->base->host; } if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme'); } public function getDefaultScheme($config, $context) { return HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme, $config, $context); } public function filter(&$uri, $config, $context) { foreach ($this->filters as $name => $f) { $result = $f->filter($uri, $config, $context); if (!$result) return false; } return true; } public function postFilter(&$uri, $config, $context) { foreach ($this->postFilters as $name => $f) { $result = $f->filter($uri, $config, $context); if (!$result) return false; } return true; } } // vim: et sw=4 sts=4 HTMLPurifier/AttrTypes.php 0000644 00000007256 15111210772 0011475 0 ustar 00 <?php /** * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects */ class HTMLPurifier_AttrTypes { /** * Lookup array of attribute string identifiers to concrete implementations. * @type HTMLPurifier_AttrDef[] */ protected $info = array(); /** * Constructs the info array, supplying default implementations for attribute * types. */ public function __construct() { // XXX This is kind of poor, since we don't actually /clone/ // instances; instead, we use the supplied make() attribute. So, // the underlying class must know how to deal with arguments. // With the old implementation of Enum, that ignored its // arguments when handling a make dispatch, the IAlign // definition wouldn't work. // pseudo-types, must be instantiated via shorthand $this->info['Enum'] = new HTMLPurifier_AttrDef_Enum(); $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); $this->info['ContentEditable'] = new HTMLPurifier_AttrDef_HTML_ContentEditable(); // unimplemented aliases $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); // "proprietary" types $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); // number is really a positive integer (one or more digits) // FIXME: ^^ not always, see start and value of list items $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); } private static function makeEnum($in) { return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); } /** * Retrieves a type * @param string $type String type name * @return HTMLPurifier_AttrDef Object AttrDef for type */ public function get($type) { // determine if there is any extra info tacked on if (strpos($type, '#') !== false) { list($type, $string) = explode('#', $type, 2); } else { $string = ''; } if (!isset($this->info[$type])) { trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); return; } return $this->info[$type]->make($string); } /** * Sets a new implementation for a type * @param string $type String type name * @param HTMLPurifier_AttrDef $impl Object AttrDef for type */ public function set($type, $impl) { $this->info[$type] = $impl; } } // vim: et sw=4 sts=4 HTMLPurifier/Printer/HTMLDefinition.php 0000644 00000024373 15111210773 0013736 0 ustar 00 <?php class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer { /** * @type HTMLPurifier_HTMLDefinition, for easy access */ protected $def; /** * @param HTMLPurifier_Config $config * @return string */ public function render($config) { $ret = ''; $this->config =& $config; $this->def = $config->getHTMLDefinition(); $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); $ret .= $this->renderDoctype(); $ret .= $this->renderEnvironment(); $ret .= $this->renderContentSets(); $ret .= $this->renderInfo(); $ret .= $this->end('div'); return $ret; } /** * Renders the Doctype table * @return string */ protected function renderDoctype() { $doctype = $this->def->doctype; $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Doctype'); $ret .= $this->row('Name', $doctype->name); $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); $ret .= $this->row('Default Modules', implode(', ', $doctype->modules)); $ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules)); $ret .= $this->end('table'); return $ret; } /** * Renders environment table, which is miscellaneous info * @return string */ protected function renderEnvironment() { $def = $this->def; $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Environment'); $ret .= $this->row('Parent of fragment', $def->info_parent); $ret .= $this->renderChildren($def->info_parent_def->child); $ret .= $this->row('Block wrap name', $def->info_block_wrapper); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Global attributes'); $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Tag transforms'); $list = array(); foreach ($def->info_tag_transform as $old => $new) { $new = $this->getClass($new, 'TagTransform_'); $list[] = "<$old> with $new"; } $ret .= $this->element('td', $this->listify($list)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Pre-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Post-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); $ret .= $this->end('tr'); $ret .= $this->end('table'); return $ret; } /** * Renders the Content Sets table * @return string */ protected function renderContentSets() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Content Sets'); foreach ($this->def->info_content_sets as $name => $lookup) { $ret .= $this->heavyHeader($name); $ret .= $this->start('tr'); $ret .= $this->element('td', $this->listifyTagLookup($lookup)); $ret .= $this->end('tr'); } $ret .= $this->end('table'); return $ret; } /** * Renders the Elements ($info) table * @return string */ protected function renderInfo() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Elements ($info)'); ksort($this->def->info); $ret .= $this->heavyHeader('Allowed tags', 2); $ret .= $this->start('tr'); $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); $ret .= $this->end('tr'); foreach ($this->def->info as $name => $def) { $ret .= $this->start('tr'); $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Inline content'); $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); $ret .= $this->end('tr'); if (!empty($def->excludes)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Excludes'); $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); $ret .= $this->end('tr'); } if (!empty($def->attr_transform_pre)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Pre-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); $ret .= $this->end('tr'); } if (!empty($def->attr_transform_post)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Post-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); $ret .= $this->end('tr'); } if (!empty($def->auto_close)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Auto closed by'); $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); $ret .= $this->end('tr'); } $ret .= $this->start('tr'); $ret .= $this->element('th', 'Allowed attributes'); $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); $ret .= $this->end('tr'); if (!empty($def->required_attr)) { $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); } $ret .= $this->renderChildren($def->child); } $ret .= $this->end('table'); return $ret; } /** * Renders a row describing the allowed children of an element * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element * @return string */ protected function renderChildren($def) { $context = new HTMLPurifier_Context(); $ret = ''; $ret .= $this->start('tr'); $elements = array(); $attr = array(); if (isset($def->elements)) { if ($def->type == 'strictblockquote') { $def->validateChildren(array(), $this->config, $context); } $elements = $def->elements; } if ($def->type == 'chameleon') { $attr['rowspan'] = 2; } elseif ($def->type == 'empty') { $elements = array(); } elseif ($def->type == 'table') { $elements = array_flip( array( 'col', 'caption', 'colgroup', 'thead', 'tfoot', 'tbody', 'tr' ) ); } $ret .= $this->element('th', 'Allowed children', $attr); if ($def->type == 'chameleon') { $ret .= $this->element( 'td', '<em>Block</em>: ' . $this->escape($this->listifyTagLookup($def->block->elements)), null, 0 ); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element( 'td', '<em>Inline</em>: ' . $this->escape($this->listifyTagLookup($def->inline->elements)), null, 0 ); } elseif ($def->type == 'custom') { $ret .= $this->element( 'td', '<em>' . ucfirst($def->type) . '</em>: ' . $def->dtd_regex ); } else { $ret .= $this->element( 'td', '<em>' . ucfirst($def->type) . '</em>: ' . $this->escape($this->listifyTagLookup($elements)), null, 0 ); } $ret .= $this->end('tr'); return $ret; } /** * Listifies a tag lookup table. * @param array $array Tag lookup array in form of array('tagname' => true) * @return string */ protected function listifyTagLookup($array) { ksort($array); $list = array(); foreach ($array as $name => $discard) { if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { continue; } $list[] = $name; } return $this->listify($list); } /** * Listifies a list of objects by retrieving class names and internal state * @param array $array List of objects * @return string * @todo Also add information about internal state */ protected function listifyObjectList($array) { ksort($array); $list = array(); foreach ($array as $obj) { $list[] = $this->getClass($obj, 'AttrTransform_'); } return $this->listify($list); } /** * Listifies a hash of attributes to AttrDef classes * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) * @return string */ protected function listifyAttr($array) { ksort($array); $list = array(); foreach ($array as $name => $obj) { if ($obj === false) { continue; } $list[] = "$name = <i>" . $this->getClass($obj, 'AttrDef_') . '</i>'; } return $this->listify($list); } /** * Creates a heavy header row * @param string $text * @param int $num * @return string */ protected function heavyHeader($text, $num = 1) { $ret = ''; $ret .= $this->start('tr'); $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); $ret .= $this->end('tr'); return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/Printer/ConfigForm.php 0000644 00000034743 15111210773 0013214 0 ustar 00 <?php /** * @todo Rewrite to use Interchange objects */ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer { /** * Printers for specific fields. * @type HTMLPurifier_Printer[] */ protected $fields = array(); /** * Documentation URL, can have fragment tagged on end. * @type string */ protected $docURL; /** * Name of form element to stuff config in. * @type string */ protected $name; /** * Whether or not to compress directive names, clipping them off * after a certain amount of letters. False to disable or integer letters * before clipping. * @type bool */ protected $compress = false; /** * @param string $name Form element name for directives to be stuffed into * @param string $doc_url String documentation URL, will have fragment tagged on * @param bool $compress Integer max length before compressing a directive name, set to false to turn off */ public function __construct( $name, $doc_url = null, $compress = false ) { parent::__construct(); $this->docURL = $doc_url; $this->name = $name; $this->compress = $compress; // initialize sub-printers $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); $this->fields[HTMLPurifier_VarParser::C_BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); } /** * Sets default column and row size for textareas in sub-printers * @param $cols Integer columns of textarea, null to use default * @param $rows Integer rows of textarea, null to use default */ public function setTextareaDimensions($cols = null, $rows = null) { if ($cols) { $this->fields['default']->cols = $cols; } if ($rows) { $this->fields['default']->rows = $rows; } } /** * Retrieves styling, in case it is not accessible by webserver */ public static function getCSS() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); } /** * Retrieves JavaScript, in case it is not accessible by webserver */ public static function getJavaScript() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); } /** * Returns HTML output for a configuration form * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array * where [0] has an HTML namespace and [1] is being rendered. * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. * @param bool $render_controls * @return string */ public function render($config, $allowed = true, $render_controls = true) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->config = $config; $this->genConfig = $gen_config; $this->prepareGenerator($gen_config); $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); $all = array(); foreach ($allowed as $key) { list($ns, $directive) = $key; $all[$ns][$directive] = $config->get($ns . '.' . $directive); } $ret = ''; $ret .= $this->start('table', array('class' => 'hp-config')); $ret .= $this->start('thead'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); foreach ($all as $ns => $directives) { $ret .= $this->renderNamespace($ns, $directives); } if ($render_controls) { $ret .= $this->start('tbody'); $ret .= $this->start('tr'); $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); $ret .= '[<a href="?">Reset</a>]'; $ret .= $this->end('td'); $ret .= $this->end('tr'); $ret .= $this->end('tbody'); } $ret .= $this->end('table'); return $ret; } /** * Renders a single namespace * @param $ns String namespace name * @param array $directives array of directives to values * @return string */ protected function renderNamespace($ns, $directives) { $ret = ''; $ret .= $this->start('tbody', array('class' => 'namespace')); $ret .= $this->start('tr'); $ret .= $this->element('th', $ns, array('colspan' => 2)); $ret .= $this->end('tr'); $ret .= $this->end('tbody'); $ret .= $this->start('tbody'); foreach ($directives as $directive => $value) { $ret .= $this->start('tr'); $ret .= $this->start('th'); if ($this->docURL) { $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); $ret .= $this->start('a', array('href' => $url)); } $attr = array('for' => "{$this->name}:$ns.$directive"); // crop directive name if it's too long if (!$this->compress || (strlen($directive) < $this->compress)) { $directive_disp = $directive; } else { $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; $attr['title'] = $directive; } $ret .= $this->element( 'label', $directive_disp, // component printers must create an element with this id $attr ); if ($this->docURL) { $ret .= $this->end('a'); } $ret .= $this->end('th'); $ret .= $this->start('td'); $def = $this->config->def->info["$ns.$directive"]; if (is_int($def)) { $allow_null = $def < 0; $type = abs($def); } else { $type = $def->type; $allow_null = isset($def->allow_null); } if (!isset($this->fields[$type])) { $type = 0; } // default $type_obj = $this->fields[$type]; if ($allow_null) { $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); } $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); $ret .= $this->end('td'); $ret .= $this->end('tr'); } $ret .= $this->end('tbody'); return $ret; } } /** * Printer decorator for directives that accept null */ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer { /** * Printer being decorated * @type HTMLPurifier_Printer */ protected $obj; /** * @param HTMLPurifier_Printer $obj Printer to decorate */ public function __construct($obj) { parent::__construct(); $this->obj = $obj; } /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_config); $ret = ''; $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); $ret .= $this->text(' Null/Disabled'); $ret .= $this->end('label'); $attr = array( 'type' => 'checkbox', 'value' => '1', 'class' => 'null-toggle', 'name' => "$name" . "[Null_$ns.$directive]", 'id' => "$name:Null_$ns.$directive", 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! ); if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { // modify inline javascript slightly $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);" . "toggleWriteability('$name:No_$ns.$directive',checked)"; } if ($value === null) { $attr['checked'] = 'checked'; } $ret .= $this->elementEmpty('input', $attr); $ret .= $this->text(' or '); $ret .= $this->elementEmpty('br'); $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); return $ret; } } /** * Swiss-army knife configuration form field printer */ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { /** * @type int */ public $cols = 18; /** * @type int */ public $rows = 5; /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_config); // this should probably be split up a little $ret = ''; $def = $config->def->info["$ns.$directive"]; if (is_int($def)) { $type = abs($def); } else { $type = $def->type; } if (is_array($value)) { switch ($type) { case HTMLPurifier_VarParser::LOOKUP: $array = $value; $value = array(); foreach ($array as $val => $b) { $value[] = $val; } //TODO does this need a break? case HTMLPurifier_VarParser::ALIST: $value = implode(PHP_EOL, $value); break; case HTMLPurifier_VarParser::HASH: $nvalue = ''; foreach ($value as $i => $v) { if (is_array($v)) { // HACK $v = implode(";", $v); } $nvalue .= "$i:$v" . PHP_EOL; } $value = $nvalue; break; default: $value = ''; } } if ($type === HTMLPurifier_VarParser::C_MIXED) { return 'Not supported'; $value = serialize($value); } $attr = array( 'name' => "$name" . "[$ns.$directive]", 'id' => "$name:$ns.$directive" ); if ($value === null) { $attr['disabled'] = 'disabled'; } if (isset($def->allowed)) { $ret .= $this->start('select', $attr); foreach ($def->allowed as $val => $b) { $attr = array(); if ($value == $val) { $attr['selected'] = 'selected'; } $ret .= $this->element('option', $val, $attr); } $ret .= $this->end('select'); } elseif ($type === HTMLPurifier_VarParser::TEXT || $type === HTMLPurifier_VarParser::ITEXT || $type === HTMLPurifier_VarParser::ALIST || $type === HTMLPurifier_VarParser::HASH || $type === HTMLPurifier_VarParser::LOOKUP) { $attr['cols'] = $this->cols; $attr['rows'] = $this->rows; $ret .= $this->start('textarea', $attr); $ret .= $this->text($value); $ret .= $this->end('textarea'); } else { $attr['value'] = $value; $attr['type'] = 'text'; $ret .= $this->elementEmpty('input', $attr); } return $ret; } } /** * Bool form field printer */ class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_config); $ret = ''; $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); $ret .= $this->text(' Yes'); $ret .= $this->end('label'); $attr = array( 'type' => 'radio', 'name' => "$name" . "[$ns.$directive]", 'id' => "$name:Yes_$ns.$directive", 'value' => '1' ); if ($value === true) { $attr['checked'] = 'checked'; } if ($value === null) { $attr['disabled'] = 'disabled'; } $ret .= $this->elementEmpty('input', $attr); $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); $ret .= $this->text(' No'); $ret .= $this->end('label'); $attr = array( 'type' => 'radio', 'name' => "$name" . "[$ns.$directive]", 'id' => "$name:No_$ns.$directive", 'value' => '0' ); if ($value === false) { $attr['checked'] = 'checked'; } if ($value === null) { $attr['disabled'] = 'disabled'; } $ret .= $this->elementEmpty('input', $attr); $ret .= $this->end('div'); return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/Printer/ConfigForm.js 0000644 00000000216 15111210773 0013025 0 ustar 00 function toggleWriteability(id_of_patient, checked) { document.getElementById(id_of_patient).disabled = checked; } // vim: et sw=4 sts=4 HTMLPurifier/Printer/ConfigForm.css 0000644 00000000455 15111210773 0013206 0 ustar 00 .hp-config {} .hp-config tbody th {text-align:right; padding-right:0.5em;} .hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} .hp-config .namespace th {text-align:center;} .hp-config .verbose {display:none;} .hp-config .controls {text-align:center;} /* vim: et sw=4 sts=4 */ HTMLPurifier/Printer/CSSDefinition.php 0000644 00000002252 15111210773 0013612 0 ustar 00 <?php class HTMLPurifier_Printer_CSSDefinition extends HTMLPurifier_Printer { /** * @type HTMLPurifier_CSSDefinition */ protected $def; /** * @param HTMLPurifier_Config $config * @return string */ public function render($config) { $this->def = $config->getCSSDefinition(); $ret = ''; $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); $ret .= $this->start('table'); $ret .= $this->element('caption', 'Properties ($info)'); $ret .= $this->start('thead'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Property', array('class' => 'heavy')); $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); ksort($this->def->info); foreach ($this->def->info as $property => $obj) { $name = $this->getClass($obj, 'AttrDef_'); $ret .= $this->row($property, $name); } $ret .= $this->end('table'); $ret .= $this->end('div'); return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/URI.php 0000644 00000024544 15111210773 0010175 0 ustar 00 <?php /** * HTML Purifier's internal representation of a URI. * @note * Internal data-structures are completely escaped. If the data needs * to be used in a non-URI context (which is very unlikely), be sure * to decode it first. The URI may not necessarily be well-formed until * validate() is called. */ class HTMLPurifier_URI { /** * @type string */ public $scheme; /** * @type string */ public $userinfo; /** * @type string */ public $host; /** * @type int */ public $port; /** * @type string */ public $path; /** * @type string */ public $query; /** * @type string */ public $fragment; /** * @param string $scheme * @param string $userinfo * @param string $host * @param int $port * @param string $path * @param string $query * @param string $fragment * @note Automatically normalizes scheme and port */ public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment) { $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme); $this->userinfo = $userinfo; $this->host = $host; $this->port = is_null($port) ? $port : (int)$port; $this->path = $path; $this->query = $query; $this->fragment = $fragment; } /** * Retrieves a scheme object corresponding to the URI's scheme/default * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_URIScheme Scheme object appropriate for validating this URI */ public function getSchemeObj($config, $context) { $registry = HTMLPurifier_URISchemeRegistry::instance(); if ($this->scheme !== null) { $scheme_obj = $registry->getScheme($this->scheme, $config, $context); if (!$scheme_obj) { return false; } // invalid scheme, clean it out } else { // no scheme: retrieve the default one $def = $config->getDefinition('URI'); $scheme_obj = $def->getDefaultScheme($config, $context); if (!$scheme_obj) { if ($def->defaultScheme !== null) { // something funky happened to the default scheme object trigger_error( 'Default scheme object "' . $def->defaultScheme . '" was not readable', E_USER_WARNING ); } // suppress error if it's null return false; } } return $scheme_obj; } /** * Generic validation method applicable for all schemes. May modify * this URI in order to get it into a compliant form. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool True if validation/filtering succeeds, false if failure */ public function validate($config, $context) { // ABNF definitions from RFC 3986 $chars_sub_delims = '!$&\'()*+,;='; $chars_gen_delims = ':/?#[]@'; $chars_pchar = $chars_sub_delims . ':@'; // validate host if (!is_null($this->host)) { $host_def = new HTMLPurifier_AttrDef_URI_Host(); $this->host = $host_def->validate($this->host, $config, $context); if ($this->host === false) { $this->host = null; } } // validate scheme // NOTE: It's not appropriate to check whether or not this // scheme is in our registry, since a URIFilter may convert a // URI that we don't allow into one we do. So instead, we just // check if the scheme can be dropped because there is no host // and it is our default scheme. if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') { // support for relative paths is pretty abysmal when the // scheme is present, so axe it when possible $def = $config->getDefinition('URI'); if ($def->defaultScheme === $this->scheme) { $this->scheme = null; } } // validate username if (!is_null($this->userinfo)) { $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':'); $this->userinfo = $encoder->encode($this->userinfo); } // validate port if (!is_null($this->port)) { if ($this->port < 1 || $this->port > 65535) { $this->port = null; } } // validate path $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/'); if (!is_null($this->host)) { // this catches $this->host === '' // path-abempty (hier and relative) // http://www.example.com/my/path // //www.example.com/my/path (looks odd, but works, and // recognized by most browsers) // (this set is valid or invalid on a scheme by scheme // basis, so we'll deal with it later) // file:///my/path // ///my/path $this->path = $segments_encoder->encode($this->path); } elseif ($this->path !== '') { if ($this->path[0] === '/') { // path-absolute (hier and relative) // http:/my/path // /my/path if (strlen($this->path) >= 2 && $this->path[1] === '/') { // This could happen if both the host gets stripped // out // http://my/path // //my/path $this->path = ''; } else { $this->path = $segments_encoder->encode($this->path); } } elseif (!is_null($this->scheme)) { // path-rootless (hier) // http:my/path // Short circuit evaluation means we don't need to check nz $this->path = $segments_encoder->encode($this->path); } else { // path-noscheme (relative) // my/path // (once again, not checking nz) $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@'); $c = strpos($this->path, '/'); if ($c !== false) { $this->path = $segment_nc_encoder->encode(substr($this->path, 0, $c)) . $segments_encoder->encode(substr($this->path, $c)); } else { $this->path = $segment_nc_encoder->encode($this->path); } } } else { // path-empty (hier and relative) $this->path = ''; // just to be safe } // qf = query and fragment $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?'); if (!is_null($this->query)) { $this->query = $qf_encoder->encode($this->query); } if (!is_null($this->fragment)) { $this->fragment = $qf_encoder->encode($this->fragment); } return true; } /** * Convert URI back to string * @return string URI appropriate for output */ public function toString() { // reconstruct authority $authority = null; // there is a rendering difference between a null authority // (http:foo-bar) and an empty string authority // (http:///foo-bar). if (!is_null($this->host)) { $authority = ''; if (!is_null($this->userinfo)) { $authority .= $this->userinfo . '@'; } $authority .= $this->host; if (!is_null($this->port)) { $authority .= ':' . $this->port; } } // Reconstruct the result // One might wonder about parsing quirks from browsers after // this reconstruction. Unfortunately, parsing behavior depends // on what *scheme* was employed (file:///foo is handled *very* // differently than http:///foo), so unfortunately we have to // defer to the schemes to do the right thing. $result = ''; if (!is_null($this->scheme)) { $result .= $this->scheme . ':'; } if (!is_null($authority)) { $result .= '//' . $authority; } $result .= $this->path; if (!is_null($this->query)) { $result .= '?' . $this->query; } if (!is_null($this->fragment)) { $result .= '#' . $this->fragment; } return $result; } /** * Returns true if this URL might be considered a 'local' URL given * the current context. This is true when the host is null, or * when it matches the host supplied to the configuration. * * Note that this does not do any scheme checking, so it is mostly * only appropriate for metadata that doesn't care about protocol * security. isBenign is probably what you actually want. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function isLocal($config, $context) { if ($this->host === null) { return true; } $uri_def = $config->getDefinition('URI'); if ($uri_def->host === $this->host) { return true; } return false; } /** * Returns true if this URL should be considered a 'benign' URL, * that is: * * - It is a local URL (isLocal), and * - It has a equal or better level of security * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function isBenign($config, $context) { if (!$this->isLocal($config, $context)) { return false; } $scheme_obj = $this->getSchemeObj($config, $context); if (!$scheme_obj) { return false; } // conservative approach $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context); if ($current_scheme_obj->secure) { if (!$scheme_obj->secure) { return false; } } return true; } } // vim: et sw=4 sts=4 HTMLPurifier/ContentSets.php 0000644 00000013010 15111210773 0011771 0 ustar 00 <?php /** * @todo Unit test */ class HTMLPurifier_ContentSets { /** * List of content set strings (pipe separators) indexed by name. * @type array */ public $info = array(); /** * List of content set lookups (element => true) indexed by name. * @type array * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets */ public $lookup = array(); /** * Synchronized list of defined content sets (keys of info). * @type array */ protected $keys = array(); /** * Synchronized list of defined content values (values of info). * @type array */ protected $values = array(); /** * Merges in module's content sets, expands identifiers in the content * sets and populates the keys, values and lookup member variables. * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule */ public function __construct($modules) { if (!is_array($modules)) { $modules = array($modules); } // populate content_sets based on module hints // sorry, no way of overloading foreach ($modules as $module) { foreach ($module->content_sets as $key => $value) { $temp = $this->convertToLookup($value); if (isset($this->lookup[$key])) { // add it into the existing content set $this->lookup[$key] = array_merge($this->lookup[$key], $temp); } else { $this->lookup[$key] = $temp; } } } $old_lookup = false; while ($old_lookup !== $this->lookup) { $old_lookup = $this->lookup; foreach ($this->lookup as $i => $set) { $add = array(); foreach ($set as $element => $x) { if (isset($this->lookup[$element])) { $add += $this->lookup[$element]; unset($this->lookup[$i][$element]); } } $this->lookup[$i] += $add; } } foreach ($this->lookup as $key => $lookup) { $this->info[$key] = implode(' | ', array_keys($lookup)); } $this->keys = array_keys($this->info); $this->values = array_values($this->info); } /** * Accepts a definition; generates and assigns a ChildDef for it * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef */ public function generateChildDef(&$def, $module) { if (!empty($def->child)) { // already done! return; } $content_model = $def->content_model; if (is_string($content_model)) { // Assume that $this->keys is alphanumeric $def->content_model = preg_replace_callback( '/\b(' . implode('|', $this->keys) . ')\b/', array($this, 'generateChildDefCallback'), $content_model ); //$def->content_model = str_replace( // $this->keys, $this->values, $content_model); } $def->child = $this->getChildDef($def, $module); } public function generateChildDefCallback($matches) { return $this->info[$matches[0]]; } /** * Instantiates a ChildDef based on content_model and content_model_type * member variables in HTMLPurifier_ElementDef * @note This will also defer to modules for custom HTMLPurifier_ChildDef * subclasses that need content set expansion * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef * @return HTMLPurifier_ChildDef corresponding to ElementDef */ public function getChildDef($def, $module) { $value = $def->content_model; if (is_object($value)) { trigger_error( 'Literal object child definitions should be stored in '. 'ElementDef->child not ElementDef->content_model', E_USER_NOTICE ); return $value; } switch ($def->content_model_type) { case 'required': return new HTMLPurifier_ChildDef_Required($value); case 'optional': return new HTMLPurifier_ChildDef_Optional($value); case 'empty': return new HTMLPurifier_ChildDef_Empty(); case 'custom': return new HTMLPurifier_ChildDef_Custom($value); } // defer to its module $return = false; if ($module->defines_child_def) { // save a func call $return = $module->getChildDef($def); } if ($return !== false) { return $return; } // error-out trigger_error( 'Could not determine which ChildDef class to instantiate', E_USER_ERROR ); return false; } /** * Converts a string list of elements separated by pipes into * a lookup array. * @param string $string List of elements * @return array Lookup array of elements */ protected function convertToLookup($string) { $array = explode('|', str_replace(' ', '', $string)); $ret = array(); foreach ($array as $k) { $ret[$k] = true; } return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/Lexer/DOMLex.php 0000644 00000030257 15111210773 0011703 0 ustar 00 <?php /** * Parser that uses PHP 5's DOM extension (part of the core). * * In PHP 5, the DOM XML extension was revamped into DOM and added to the core. * It gives us a forgiving HTML parser, which we use to transform the HTML * into a DOM, and then into the tokens. It is blazingly fast (for large * documents, it performs twenty times faster than * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5. * * @note Any empty elements will have empty tokens associated with them, even if * this is prohibited by the spec. This is cannot be fixed until the spec * comes into play. * * @note PHP's DOM extension does not actually parse any entities, we use * our own function to do that. * * @warning DOM tends to drop whitespace, which may wreak havoc on indenting. * If this is a huge problem, due to the fact that HTML is hand * edited and you are unable to get a parser cache that caches the * the output of HTML Purifier while keeping the original HTML lying * around, you may want to run Tidy on the resulting output or use * HTMLPurifier_DirectLex */ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer { /** * @type HTMLPurifier_TokenFactory */ private $factory; public function __construct() { // setup the factory parent::__construct(); $this->factory = new HTMLPurifier_TokenFactory(); } /** * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_Token[] */ public function tokenizeHTML($html, $config, $context) { $html = $this->normalize($html, $config, $context); // attempt to armor stray angled brackets that cannot possibly // form tags and thus are probably being used as emoticons if ($config->get('Core.AggressivelyFixLt')) { $char = '[^a-z!\/]'; $comment = "/<!--(.*?)(-->|\z)/is"; $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html); do { $old = $html; $html = preg_replace("/<($char)/i", '<\\1', $html); } while ($html !== $old); $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments } // preprocess html, essential for UTF-8 $html = $this->wrapHTML($html, $config, $context); $doc = new DOMDocument(); $doc->encoding = 'UTF-8'; // theoretically, the above has this covered $options = 0; if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) { $options |= LIBXML_PARSEHUGE; } set_error_handler(array($this, 'muteErrorHandler')); // loadHTML() fails on PHP 5.3 when second parameter is given if ($options) { $doc->loadHTML($html, $options); } else { $doc->loadHTML($html); } restore_error_handler(); $body = $doc->getElementsByTagName('html')->item(0)-> // <html> getElementsByTagName('body')->item(0); // <body> $div = $body->getElementsByTagName('div')->item(0); // <div> $tokens = array(); $this->tokenizeDOM($div, $tokens, $config); // If the div has a sibling, that means we tripped across // a premature </div> tag. So remove the div we parsed, // and then tokenize the rest of body. We can't tokenize // the sibling directly as we'll lose the tags in that case. if ($div->nextSibling) { $body->removeChild($div); $this->tokenizeDOM($body, $tokens, $config); } return $tokens; } /** * Iterative function that tokenizes a node, putting it into an accumulator. * To iterate is human, to recurse divine - L. Peter Deutsch * @param DOMNode $node DOMNode to be tokenized. * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. * @return HTMLPurifier_Token of node appended to previously passed tokens. */ protected function tokenizeDOM($node, &$tokens, $config) { $level = 0; $nodes = array($level => new HTMLPurifier_Queue(array($node))); $closingNodes = array(); do { while (!$nodes[$level]->isEmpty()) { $node = $nodes[$level]->shift(); // FIFO $collect = $level > 0 ? true : false; $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config); if ($needEndingTag) { $closingNodes[$level][] = $node; } if ($node->childNodes && $node->childNodes->length) { $level++; $nodes[$level] = new HTMLPurifier_Queue(); foreach ($node->childNodes as $childNode) { $nodes[$level]->push($childNode); } } } $level--; if ($level && isset($closingNodes[$level])) { while ($node = array_pop($closingNodes[$level])) { $this->createEndNode($node, $tokens); } } } while ($level > 0); } /** * Portably retrieve the tag name of a node; deals with older versions * of libxml like 2.7.6 * @param DOMNode $node */ protected function getTagName($node) { if (isset($node->tagName)) { return $node->tagName; } else if (isset($node->nodeName)) { return $node->nodeName; } else if (isset($node->localName)) { return $node->localName; } return null; } /** * Portably retrieve the data of a node; deals with older versions * of libxml like 2.7.6 * @param DOMNode $node */ protected function getData($node) { if (isset($node->data)) { return $node->data; } else if (isset($node->nodeValue)) { return $node->nodeValue; } else if (isset($node->textContent)) { return $node->textContent; } return null; } /** * @param DOMNode $node DOMNode to be tokenized. * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. * @param bool $collect Says whether or start and close are collected, set to * false at first recursion because it's the implicit DIV * tag you're dealing with. * @return bool if the token needs an endtoken * @todo data and tagName properties don't seem to exist in DOMNode? */ protected function createStartNode($node, &$tokens, $collect, $config) { // intercept non element nodes. WE MUST catch all of them, // but we're not getting the character reference nodes because // those should have been preprocessed if ($node->nodeType === XML_TEXT_NODE) { $data = $this->getData($node); // Handle variable data property if ($data !== null) { $tokens[] = $this->factory->createText($data); } return false; } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) { // undo libxml's special treatment of <script> and <style> tags $last = end($tokens); $data = $node->data; // (note $node->tagname is already normalized) if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) { $new_data = trim($data); if (substr($new_data, 0, 4) === '<!--') { $data = substr($new_data, 4); if (substr($data, -3) === '-->') { $data = substr($data, 0, -3); } else { // Highly suspicious! Not sure what to do... } } } $tokens[] = $this->factory->createText($this->parseText($data, $config)); return false; } elseif ($node->nodeType === XML_COMMENT_NODE) { // this is code is only invoked for comments in script/style in versions // of libxml pre-2.6.28 (regular comments, of course, are still // handled regularly) $tokens[] = $this->factory->createComment($node->data); return false; } elseif ($node->nodeType !== XML_ELEMENT_NODE) { // not-well tested: there may be other nodes we have to grab return false; } $attr = $node->hasAttributes() ? $this->transformAttrToAssoc($node->attributes) : array(); $tag_name = $this->getTagName($node); // Handle variable tagName property if (empty($tag_name)) { return (bool) $node->childNodes->length; } // We still have to make sure that the element actually IS empty if (!$node->childNodes->length) { if ($collect) { $tokens[] = $this->factory->createEmpty($tag_name, $attr); } return false; } else { if ($collect) { $tokens[] = $this->factory->createStart($tag_name, $attr); } return true; } } /** * @param DOMNode $node * @param HTMLPurifier_Token[] $tokens */ protected function createEndNode($node, &$tokens) { $tag_name = $this->getTagName($node); // Handle variable tagName property $tokens[] = $this->factory->createEnd($tag_name); } /** * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array. * * @param DOMNamedNodeMap $node_map DOMNamedNodeMap of DOMAttr objects. * @return array Associative array of attributes. */ protected function transformAttrToAssoc($node_map) { // NamedNodeMap is documented very well, so we're using undocumented // features, namely, the fact that it implements Iterator and // has a ->length attribute if ($node_map->length === 0) { return array(); } $array = array(); foreach ($node_map as $attr) { $array[$attr->name] = $attr->value; } return $array; } /** * An error handler that mutes all errors * @param int $errno * @param string $errstr */ public function muteErrorHandler($errno, $errstr) { } /** * Callback function for undoing escaping of stray angled brackets * in comments * @param array $matches * @return string */ public function callbackUndoCommentSubst($matches) { return '<!--' . strtr($matches[1], array('&' => '&', '<' => '<')) . $matches[2]; } /** * Callback function that entity-izes ampersands in comments so that * callbackUndoCommentSubst doesn't clobber them * @param array $matches * @return string */ public function callbackArmorCommentEntities($matches) { return '<!--' . str_replace('&', '&', $matches[1]) . $matches[2]; } /** * Wraps an HTML fragment in the necessary HTML * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return string */ protected function wrapHTML($html, $config, $context, $use_div = true) { $def = $config->getDefinition('HTML'); $ret = ''; if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) { $ret .= '<!DOCTYPE html '; if (!empty($def->doctype->dtdPublic)) { $ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" '; } if (!empty($def->doctype->dtdSystem)) { $ret .= '"' . $def->doctype->dtdSystem . '" '; } $ret .= '>'; } $ret .= '<html><head>'; $ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'; // No protection if $html contains a stray </div>! $ret .= '</head><body>'; if ($use_div) $ret .= '<div>'; $ret .= $html; if ($use_div) $ret .= '</div>'; $ret .= '</body></html>'; return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/Lexer/DirectLex.php 0000644 00000050022 15111210773 0012466 0 ustar 00 <?php /** * Our in-house implementation of a parser. * * A pure PHP parser, DirectLex has absolutely no dependencies, making * it a reasonably good default for PHP4. Written with efficiency in mind, * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it * pales in comparison to HTMLPurifier_Lexer_DOMLex. * * @todo Reread XML spec and document differences. */ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer { /** * @type bool */ public $tracksLineNumbers = true; /** * Whitespace characters for str(c)spn. * @type string */ protected $_whitespace = "\x20\x09\x0D\x0A"; /** * Callback function for script CDATA fudge * @param array $matches, in form of array(opening tag, contents, closing tag) * @return string */ protected function scriptCallback($matches) { return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3]; } /** * @param String $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array|HTMLPurifier_Token[] */ public function tokenizeHTML($html, $config, $context) { // special normalization for script tags without any armor // our "armor" heurstic is a < sign any number of whitespaces after // the first script tag if ($config->get('HTML.Trusted')) { $html = preg_replace_callback( '#(<script[^>]*>)(\s*[^<].+?)(</script>)#si', array($this, 'scriptCallback'), $html ); } $html = $this->normalize($html, $config, $context); $cursor = 0; // our location in the text $inside_tag = false; // whether or not we're parsing the inside of a tag $array = array(); // result array // This is also treated to mean maintain *column* numbers too $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); if ($maintain_line_numbers === null) { // automatically determine line numbering by checking // if error collection is on $maintain_line_numbers = $config->get('Core.CollectErrors'); } if ($maintain_line_numbers) { $current_line = 1; $current_col = 0; $length = strlen($html); } else { $current_line = false; $current_col = false; $length = false; } $context->register('CurrentLine', $current_line); $context->register('CurrentCol', $current_col); $nl = "\n"; // how often to manually recalculate. This will ALWAYS be right, // but it's pretty wasteful. Set to 0 to turn off $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); $e = false; if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } // for testing synchronization $loops = 0; while (++$loops) { // $cursor is either at the start of a token, or inside of // a tag (i.e. there was a < immediately before it), as indicated // by $inside_tag if ($maintain_line_numbers) { // $rcursor, however, is always at the start of a token. $rcursor = $cursor - (int)$inside_tag; // Column number is cheap, so we calculate it every round. // We're interested at the *end* of the newline string, so // we need to add strlen($nl) == 1 to $nl_pos before subtracting it // from our "rcursor" position. $nl_pos = strrpos($html, $nl, $rcursor - $length); $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); // recalculate lines if ($synchronize_interval && // synchronization is on $cursor > 0 && // cursor is further than zero $loops % $synchronize_interval === 0) { // time to synchronize! $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); } } $position_next_lt = strpos($html, '<', $cursor); $position_next_gt = strpos($html, '>', $cursor); // triggers on "<b>asdf</b>" but not "asdf <b></b>" // special case to set up context if ($position_next_lt === $cursor) { $inside_tag = true; $cursor++; } if (!$inside_tag && $position_next_lt !== false) { // We are not inside tag and there still is another tag to parse $token = new HTMLPurifier_Token_Text( $this->parseText( substr( $html, $cursor, $position_next_lt - $cursor ), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); } $array[] = $token; $cursor = $position_next_lt + 1; $inside_tag = true; continue; } elseif (!$inside_tag) { // We are not inside tag but there are no more tags // If we're already at the end, break if ($cursor === strlen($html)) { break; } // Create Text of rest of string $token = new HTMLPurifier_Token_Text( $this->parseText( substr( $html, $cursor ), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); } $array[] = $token; break; } elseif ($inside_tag && $position_next_gt !== false) { // We are in tag and it is well formed // Grab the internals of the tag $strlen_segment = $position_next_gt - $cursor; if ($strlen_segment < 1) { // there's nothing to process! $token = new HTMLPurifier_Token_Text('<'); $cursor++; continue; } $segment = substr($html, $cursor, $strlen_segment); if ($segment === false) { // somehow, we attempted to access beyond the end of // the string, defense-in-depth, reported by Nate Abele break; } // Check if it's a comment if (substr($segment, 0, 3) === '!--') { // re-determine segment length, looking for --> $position_comment_end = strpos($html, '-->', $cursor); if ($position_comment_end === false) { // uh oh, we have a comment that extends to // infinity. Can't be helped: set comment // end position to end of string if ($e) { $e->send(E_WARNING, 'Lexer: Unclosed comment'); } $position_comment_end = strlen($html); $end = true; } else { $end = false; } $strlen_segment = $position_comment_end - $cursor; $segment = substr($html, $cursor, $strlen_segment); $token = new HTMLPurifier_Token_Comment( substr( $segment, 3, $strlen_segment - 3 ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); } $array[] = $token; $cursor = $end ? $position_comment_end : $position_comment_end + 3; $inside_tag = false; continue; } // Check if it's an end tag $is_end_tag = (strpos($segment, '/') === 0); if ($is_end_tag) { $type = substr($segment, 1); $token = new HTMLPurifier_Token_End($type); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; } // Check leading character is alnum, if not, we may // have accidently grabbed an emoticon. Translate into // text and go our merry way if (!ctype_alpha($segment[0])) { // XML: $segment[0] !== '_' && $segment[0] !== ':' if ($e) { $e->send(E_NOTICE, 'Lexer: Unescaped lt'); } $token = new HTMLPurifier_Token_Text('<'); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; continue; } // Check if it is explicitly self closing, if so, remove // trailing slash. Remember, we could have a tag like <br>, so // any later token processing scripts must convert improperly // classified EmptyTags from StartTags. $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); if ($is_self_closing) { $strlen_segment--; $segment = substr($segment, 0, $strlen_segment); } // Check if there are any attributes $position_first_space = strcspn($segment, $this->_whitespace); if ($position_first_space >= $strlen_segment) { if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($segment); } else { $token = new HTMLPurifier_Token_Start($segment); } if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; } // Grab out all the data $type = substr($segment, 0, $position_first_space); $attribute_string = trim( substr( $segment, $position_first_space ) ); if ($attribute_string) { $attr = $this->parseAttributeString( $attribute_string, $config, $context ); } else { $attr = array(); } if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($type, $attr); } else { $token = new HTMLPurifier_Token_Start($type, $attr); } if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $cursor = $position_next_gt + 1; $inside_tag = false; continue; } else { // inside tag, but there's no ending > sign if ($e) { $e->send(E_WARNING, 'Lexer: Missing gt'); } $token = new HTMLPurifier_Token_Text( '<' . $this->parseText( substr($html, $cursor), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); } // no cursor scroll? Hmm... $array[] = $token; break; } break; } $context->destroy('CurrentLine'); $context->destroy('CurrentCol'); return $array; } /** * PHP 5.0.x compatible substr_count that implements offset and length * @param string $haystack * @param string $needle * @param int $offset * @param int $length * @return int */ protected function substrCount($haystack, $needle, $offset, $length) { static $oldVersion; if ($oldVersion === null) { $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); } if ($oldVersion) { $haystack = substr($haystack, $offset, $length); return substr_count($haystack, $needle); } else { return substr_count($haystack, $needle, $offset, $length); } } /** * Takes the inside of an HTML tag and makes an assoc array of attributes. * * @param string $string Inside of tag excluding name. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array Assoc array of attributes. */ public function parseAttributeString($string, $config, $context) { $string = (string)$string; // quick typecast if ($string == '') { return array(); } // no attributes $e = false; if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } // let's see if we can abort as quickly as possible // one equal sign, no spaces => one attribute $num_equal = substr_count($string, '='); $has_space = strpos($string, ' '); if ($num_equal === 0 && !$has_space) { // bool attribute return array($string => $string); } elseif ($num_equal === 1 && !$has_space) { // only one attribute list($key, $quoted_value) = explode('=', $string); $quoted_value = trim($quoted_value); if (!$key) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } return array(); } if (!$quoted_value) { return array($key => ''); } $first_char = @$quoted_value[0]; $last_char = @$quoted_value[strlen($quoted_value) - 1]; $same_quote = ($first_char == $last_char); $open_quote = ($first_char == '"' || $first_char == "'"); if ($same_quote && $open_quote) { // well behaved $value = substr($quoted_value, 1, strlen($quoted_value) - 2); } else { // not well behaved if ($open_quote) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing end quote'); } $value = substr($quoted_value, 1); } else { $value = $quoted_value; } } if ($value === false) { $value = ''; } return array($key => $this->parseAttr($value, $config)); } // setup loop environment $array = array(); // return assoc array of attributes $cursor = 0; // current position in string (moves forward) $size = strlen($string); // size of the string (stays the same) // if we have unquoted attributes, the parser expects a terminating // space, so let's guarantee that there's always a terminating space. $string .= ' '; $old_cursor = -1; while ($cursor < $size) { if ($old_cursor >= $cursor) { throw new Exception("Infinite loop detected"); } $old_cursor = $cursor; $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); // grab the key $key_begin = $cursor; //we're currently at the start of the key // scroll past all characters that are the key (not whitespace or =) $cursor += strcspn($string, $this->_whitespace . '=', $cursor); $key_end = $cursor; // now at the end of the key $key = substr($string, $key_begin, $key_end - $key_begin); if (!$key) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop continue; // empty key } // scroll past all whitespace $cursor += strspn($string, $this->_whitespace, $cursor); if ($cursor >= $size) { $array[$key] = $key; break; } // if the next character is an equal sign, we've got a regular // pair, otherwise, it's a bool attribute $first_char = @$string[$cursor]; if ($first_char == '=') { // key="value" $cursor++; $cursor += strspn($string, $this->_whitespace, $cursor); if ($cursor === false) { $array[$key] = ''; break; } // we might be in front of a quote right now $char = @$string[$cursor]; if ($char == '"' || $char == "'") { // it's quoted, end bound is $char $cursor++; $value_begin = $cursor; $cursor = strpos($string, $char, $cursor); $value_end = $cursor; } else { // it's not quoted, end bound is whitespace $value_begin = $cursor; $cursor += strcspn($string, $this->_whitespace, $cursor); $value_end = $cursor; } // we reached a premature end if ($cursor === false) { $cursor = $size; $value_end = $cursor; } $value = substr($string, $value_begin, $value_end - $value_begin); if ($value === false) { $value = ''; } $array[$key] = $this->parseAttr($value, $config); $cursor++; } else { // boolattr if ($key !== '') { $array[$key] = $key; } else { // purely theoretical if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } } } } return $array; } } // vim: et sw=4 sts=4 HTMLPurifier/Lexer/PH5P.php 0000644 00000544475 15111210773 0011343 0 ustar 00 <?php /** * Experimental HTML5-based parser using Jeroen van der Meer's PH5P library. * Occupies space in the HTML5 pseudo-namespace, which may cause conflicts. * * @note * Recent changes to PHP's DOM extension have resulted in some fatal * error conditions with the original version of PH5P. Pending changes, * this lexer will punt to DirectLex if DOM throws an exception. */ class HTMLPurifier_Lexer_PH5P extends HTMLPurifier_Lexer_DOMLex { /** * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_Token[] */ public function tokenizeHTML($html, $config, $context) { $new_html = $this->normalize($html, $config, $context); $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); try { $parser = new HTML5($new_html); $doc = $parser->save(); } catch (DOMException $e) { // Uh oh, it failed. Punt to DirectLex. $lexer = new HTMLPurifier_Lexer_DirectLex(); $context->register('PH5PError', $e); // save the error, so we can detect it return $lexer->tokenizeHTML($html, $config, $context); // use original HTML } $tokens = array(); $this->tokenizeDOM( $doc->getElementsByTagName('html')->item(0)-> // <html> getElementsByTagName('body')->item(0) // <body> , $tokens, $config ); return $tokens; } } /* Copyright 2007 Jeroen van der Meer <http://jero.net/> 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. */ class HTML5 { private $data; private $char; private $EOF; private $state; private $tree; private $token; private $content_model; private $escape = false; private $entities = array( 'AElig;', 'AElig', 'AMP;', 'AMP', 'Aacute;', 'Aacute', 'Acirc;', 'Acirc', 'Agrave;', 'Agrave', 'Alpha;', 'Aring;', 'Aring', 'Atilde;', 'Atilde', 'Auml;', 'Auml', 'Beta;', 'COPY;', 'COPY', 'Ccedil;', 'Ccedil', 'Chi;', 'Dagger;', 'Delta;', 'ETH;', 'ETH', 'Eacute;', 'Eacute', 'Ecirc;', 'Ecirc', 'Egrave;', 'Egrave', 'Epsilon;', 'Eta;', 'Euml;', 'Euml', 'GT;', 'GT', 'Gamma;', 'Iacute;', 'Iacute', 'Icirc;', 'Icirc', 'Igrave;', 'Igrave', 'Iota;', 'Iuml;', 'Iuml', 'Kappa;', 'LT;', 'LT', 'Lambda;', 'Mu;', 'Ntilde;', 'Ntilde', 'Nu;', 'OElig;', 'Oacute;', 'Oacute', 'Ocirc;', 'Ocirc', 'Ograve;', 'Ograve', 'Omega;', 'Omicron;', 'Oslash;', 'Oslash', 'Otilde;', 'Otilde', 'Ouml;', 'Ouml', 'Phi;', 'Pi;', 'Prime;', 'Psi;', 'QUOT;', 'QUOT', 'REG;', 'REG', 'Rho;', 'Scaron;', 'Sigma;', 'THORN;', 'THORN', 'TRADE;', 'Tau;', 'Theta;', 'Uacute;', 'Uacute', 'Ucirc;', 'Ucirc', 'Ugrave;', 'Ugrave', 'Upsilon;', 'Uuml;', 'Uuml', 'Xi;', 'Yacute;', 'Yacute', 'Yuml;', 'Zeta;', 'aacute;', 'aacute', 'acirc;', 'acirc', 'acute;', 'acute', 'aelig;', 'aelig', 'agrave;', 'agrave', 'alefsym;', 'alpha;', 'amp;', 'amp', 'and;', 'ang;', 'apos;', 'aring;', 'aring', 'asymp;', 'atilde;', 'atilde', 'auml;', 'auml', 'bdquo;', 'beta;', 'brvbar;', 'brvbar', 'bull;', 'cap;', 'ccedil;', 'ccedil', 'cedil;', 'cedil', 'cent;', 'cent', 'chi;', 'circ;', 'clubs;', 'cong;', 'copy;', 'copy', 'crarr;', 'cup;', 'curren;', 'curren', 'dArr;', 'dagger;', 'darr;', 'deg;', 'deg', 'delta;', 'diams;', 'divide;', 'divide', 'eacute;', 'eacute', 'ecirc;', 'ecirc', 'egrave;', 'egrave', 'empty;', 'emsp;', 'ensp;', 'epsilon;', 'equiv;', 'eta;', 'eth;', 'eth', 'euml;', 'euml', 'euro;', 'exist;', 'fnof;', 'forall;', 'frac12;', 'frac12', 'frac14;', 'frac14', 'frac34;', 'frac34', 'frasl;', 'gamma;', 'ge;', 'gt;', 'gt', 'hArr;', 'harr;', 'hearts;', 'hellip;', 'iacute;', 'iacute', 'icirc;', 'icirc', 'iexcl;', 'iexcl', 'igrave;', 'igrave', 'image;', 'infin;', 'int;', 'iota;', 'iquest;', 'iquest', 'isin;', 'iuml;', 'iuml', 'kappa;', 'lArr;', 'lambda;', 'lang;', 'laquo;', 'laquo', 'larr;', 'lceil;', 'ldquo;', 'le;', 'lfloor;', 'lowast;', 'loz;', 'lrm;', 'lsaquo;', 'lsquo;', 'lt;', 'lt', 'macr;', 'macr', 'mdash;', 'micro;', 'micro', 'middot;', 'middot', 'minus;', 'mu;', 'nabla;', 'nbsp;', 'nbsp', 'ndash;', 'ne;', 'ni;', 'not;', 'not', 'notin;', 'nsub;', 'ntilde;', 'ntilde', 'nu;', 'oacute;', 'oacute', 'ocirc;', 'ocirc', 'oelig;', 'ograve;', 'ograve', 'oline;', 'omega;', 'omicron;', 'oplus;', 'or;', 'ordf;', 'ordf', 'ordm;', 'ordm', 'oslash;', 'oslash', 'otilde;', 'otilde', 'otimes;', 'ouml;', 'ouml', 'para;', 'para', 'part;', 'permil;', 'perp;', 'phi;', 'pi;', 'piv;', 'plusmn;', 'plusmn', 'pound;', 'pound', 'prime;', 'prod;', 'prop;', 'psi;', 'quot;', 'quot', 'rArr;', 'radic;', 'rang;', 'raquo;', 'raquo', 'rarr;', 'rceil;', 'rdquo;', 'real;', 'reg;', 'reg', 'rfloor;', 'rho;', 'rlm;', 'rsaquo;', 'rsquo;', 'sbquo;', 'scaron;', 'sdot;', 'sect;', 'sect', 'shy;', 'shy', 'sigma;', 'sigmaf;', 'sim;', 'spades;', 'sub;', 'sube;', 'sum;', 'sup1;', 'sup1', 'sup2;', 'sup2', 'sup3;', 'sup3', 'sup;', 'supe;', 'szlig;', 'szlig', 'tau;', 'there4;', 'theta;', 'thetasym;', 'thinsp;', 'thorn;', 'thorn', 'tilde;', 'times;', 'times', 'trade;', 'uArr;', 'uacute;', 'uacute', 'uarr;', 'ucirc;', 'ucirc', 'ugrave;', 'ugrave', 'uml;', 'uml', 'upsih;', 'upsilon;', 'uuml;', 'uuml', 'weierp;', 'xi;', 'yacute;', 'yacute', 'yen;', 'yen', 'yuml;', 'yuml', 'zeta;', 'zwj;', 'zwnj;' ); const PCDATA = 0; const RCDATA = 1; const CDATA = 2; const PLAINTEXT = 3; const DOCTYPE = 0; const STARTTAG = 1; const ENDTAG = 2; const COMMENT = 3; const CHARACTR = 4; const EOF = 5; public function __construct($data) { $this->data = $data; $this->char = -1; $this->EOF = strlen($data); $this->tree = new HTML5TreeConstructer; $this->content_model = self::PCDATA; $this->state = 'data'; while ($this->state !== null) { $this->{$this->state . 'State'}(); } } public function save() { return $this->tree->save(); } private function char() { return ($this->char < $this->EOF) ? $this->data[$this->char] : false; } private function character($s, $l = 0) { if ($s + $l < $this->EOF) { if ($l === 0) { return $this->data[$s]; } else { return substr($this->data, $s, $l); } } } private function characters($char_class, $start) { return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); } private function dataState() { // Consume the next input character $this->char++; $char = $this->char(); if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { /* U+0026 AMPERSAND (&) When the content model flag is set to one of the PCDATA or RCDATA states: switch to the entity data state. Otherwise: treat it as per the "anything else" entry below. */ $this->state = 'entityData'; } elseif ($char === '-') { /* If the content model flag is set to either the RCDATA state or the CDATA state, and the escape flag is false, and there are at least three characters before this one in the input stream, and the last four characters in the input stream, including this one, are U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, and U+002D HYPHEN-MINUS ("<!--"), then set the escape flag to true. */ if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && $this->escape === false && $this->char >= 3 && $this->character($this->char - 4, 4) === '<!--' ) { $this->escape = true; } /* In any case, emit the input character as a character token. Stay in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); /* U+003C LESS-THAN SIGN (<) */ } elseif ($char === '<' && ($this->content_model === self::PCDATA || (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && $this->escape === false)) ) { /* When the content model flag is set to the PCDATA state: switch to the tag open state. When the content model flag is set to either the RCDATA state or the CDATA state and the escape flag is false: switch to the tag open state. Otherwise: treat it as per the "anything else" entry below. */ $this->state = 'tagOpen'; /* U+003E GREATER-THAN SIGN (>) */ } elseif ($char === '>') { /* If the content model flag is set to either the RCDATA state or the CDATA state, and the escape flag is true, and the last three characters in the input stream including this one are U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"), set the escape flag to false. */ if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && $this->escape === true && $this->character($this->char, 3) === '-->' ) { $this->escape = false; } /* In any case, emit the input character as a character token. Stay in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); } elseif ($this->char === $this->EOF) { /* EOF Emit an end-of-file token. */ $this->EOF(); } elseif ($this->content_model === self::PLAINTEXT) { /* When the content model flag is set to the PLAINTEXT state THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of the text and emit it as a character token. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => substr($this->data, $this->char) ) ); $this->EOF(); } else { /* Anything else THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that otherwise would also be treated as a character token and emit it as a single character token. Stay in the data state. */ $len = strcspn($this->data, '<&', $this->char); $char = substr($this->data, $this->char, $len); $this->char += $len - 1; $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); $this->state = 'data'; } } private function entityDataState() { // Attempt to consume an entity. $entity = $this->entity(); // If nothing is returned, emit a U+0026 AMPERSAND character token. // Otherwise, emit the character token that was returned. $char = (!$entity) ? '&' : $entity; $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); // Finally, switch to the data state. $this->state = 'data'; } private function tagOpenState() { switch ($this->content_model) { case self::RCDATA: case self::CDATA: /* If the next input character is a U+002F SOLIDUS (/) character, consume it and switch to the close tag open state. If the next input character is not a U+002F SOLIDUS (/) character, emit a U+003C LESS-THAN SIGN character token and switch to the data state to process the next input character. */ if ($this->character($this->char + 1) === '/') { $this->char++; $this->state = 'closeTagOpen'; } else { $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<' ) ); $this->state = 'data'; } break; case self::PCDATA: // If the content model flag is set to the PCDATA state // Consume the next input character: $this->char++; $char = $this->char(); if ($char === '!') { /* U+0021 EXCLAMATION MARK (!) Switch to the markup declaration open state. */ $this->state = 'markupDeclarationOpen'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Switch to the close tag open state. */ $this->state = 'closeTagOpen'; } elseif (preg_match('/^[A-Za-z]$/', $char)) { /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z Create a new start tag token, set its tag name to the lowercase version of the input character (add 0x0020 to the character's code point), then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.) */ $this->token = array( 'name' => strtolower($char), 'type' => self::STARTTAG, 'attr' => array() ); $this->state = 'tagName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+003E GREATER-THAN SIGN character token. Switch to the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<>' ) ); $this->state = 'data'; } elseif ($char === '?') { /* U+003F QUESTION MARK (?) Parse error. Switch to the bogus comment state. */ $this->state = 'bogusComment'; } else { /* Anything else Parse error. Emit a U+003C LESS-THAN SIGN character token and reconsume the current input character in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<' ) ); $this->char--; $this->state = 'data'; } break; } } private function closeTagOpenState() { $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && (!$the_same || ($the_same && (!preg_match( '/[\t\n\x0b\x0c >\/]/', $this->character($this->char + 1 + strlen($next_node)) ) || $this->EOF === $this->char))) ) { /* If the content model flag is set to the RCDATA or CDATA states then examine the next few characters. If they do not match the tag name of the last start tag token emitted (case insensitively), or if they do but they are not immediately followed by one of the following characters: * U+0009 CHARACTER TABULATION * U+000A LINE FEED (LF) * U+000B LINE TABULATION * U+000C FORM FEED (FF) * U+0020 SPACE * U+003E GREATER-THAN SIGN (>) * U+002F SOLIDUS (/) * EOF ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character token, and switch to the data state to process the next input character. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '</' ) ); $this->state = 'data'; } else { /* Otherwise, if the content model flag is set to the PCDATA state, or if the next few characters do match that tag name, consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[A-Za-z]$/', $char)) { /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z Create a new end tag token, set its tag name to the lowercase version of the input character (add 0x0020 to the character's code point), then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.) */ $this->token = array( 'name' => strtolower($char), 'type' => self::ENDTAG ); $this->state = 'tagName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Parse error. Switch to the data state. */ $this->state = 'data'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS character token. Reconsume the EOF character in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '</' ) ); $this->char--; $this->state = 'data'; } else { /* Parse error. Switch to the bogus comment state. */ $this->state = 'bogusComment'; } } } private function tagNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } else { /* Anything else Append the current input character to the current tag token's tag name. Stay in the tag name state. */ $this->token['name'] .= strtolower($char); $this->state = 'tagName'; } } private function beforeAttributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Stay in the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Start a new attribute in the current tag token. Set that attribute's name to the current input character, and its value to the empty string. Switch to the attribute name state. */ $this->token['attr'][] = array( 'name' => strtolower($char), 'value' => null ); $this->state = 'attributeName'; } } private function attributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute name state. */ $this->state = 'afterAttributeName'; } elseif ($char === '=') { /* U+003D EQUALS SIGN (=) Switch to the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's name. Stay in the attribute name state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['name'] .= strtolower($char); $this->state = 'attributeName'; } } private function afterAttributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the after attribute name state. */ $this->state = 'afterAttributeName'; } elseif ($char === '=') { /* U+003D EQUALS SIGN (=) Switch to the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Start a new attribute in the current tag token. Set that attribute's name to the current input character, and its value to the empty string. Switch to the attribute name state. */ $this->token['attr'][] = array( 'name' => strtolower($char), 'value' => null ); $this->state = 'attributeName'; } } private function beforeAttributeValueState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '"') { /* U+0022 QUOTATION MARK (") Switch to the attribute value (double-quoted) state. */ $this->state = 'attributeValueDoubleQuoted'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the attribute value (unquoted) state and reconsume this input character. */ $this->char--; $this->state = 'attributeValueUnquoted'; } elseif ($char === '\'') { /* U+0027 APOSTROPHE (') Switch to the attribute value (single-quoted) state. */ $this->state = 'attributeValueSingleQuoted'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Switch to the attribute value (unquoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueUnquoted'; } } private function attributeValueDoubleQuotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if ($char === '"') { /* U+0022 QUOTATION MARK (") Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState('double'); } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (double-quoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueDoubleQuoted'; } } private function attributeValueSingleQuotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if ($char === '\'') { /* U+0022 QUOTATION MARK (') Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState('single'); } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (single-quoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueSingleQuoted'; } } private function attributeValueUnquotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState(); } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (unquoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueUnquoted'; } } private function entityInAttributeValueState() { // Attempt to consume an entity. $entity = $this->entity(); // If nothing is returned, append a U+0026 AMPERSAND character to the // current attribute's value. Otherwise, emit the character token that // was returned. $char = (!$entity) ? '&' : $entity; $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; } private function bogusCommentState() { /* Consume every character up to the first U+003E GREATER-THAN SIGN character (>) or the end of the file (EOF), whichever comes first. Emit a comment token whose data is the concatenation of all the characters starting from and including the character that caused the state machine to switch into the bogus comment state, up to and including the last consumed character before the U+003E character, if any, or up to the end of the file otherwise. (If the comment was started by the end of the file (EOF), the token is empty.) */ $data = $this->characters('^>', $this->char); $this->emitToken( array( 'data' => $data, 'type' => self::COMMENT ) ); $this->char += strlen($data); /* Switch to the data state. */ $this->state = 'data'; /* If the end of the file was reached, reconsume the EOF character. */ if ($this->char === $this->EOF) { $this->char = $this->EOF - 1; } } private function markupDeclarationOpenState() { /* If the next two characters are both U+002D HYPHEN-MINUS (-) characters, consume those two characters, create a comment token whose data is the empty string, and switch to the comment state. */ if ($this->character($this->char + 1, 2) === '--') { $this->char += 2; $this->state = 'comment'; $this->token = array( 'data' => null, 'type' => self::COMMENT ); /* Otherwise if the next seven chacacters are a case-insensitive match for the word "DOCTYPE", then consume those characters and switch to the DOCTYPE state. */ } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { $this->char += 7; $this->state = 'doctype'; /* Otherwise, is is a parse error. Switch to the bogus comment state. The next character that is consumed, if any, is the first character that will be in the comment. */ } else { $this->char++; $this->state = 'bogusComment'; } } private function commentState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); /* U+002D HYPHEN-MINUS (-) */ if ($char === '-') { /* Switch to the comment dash state */ $this->state = 'commentDash'; /* EOF */ } elseif ($this->char === $this->EOF) { /* Parse error. Emit the comment token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; /* Anything else */ } else { /* Append the input character to the comment token's data. Stay in the comment state. */ $this->token['data'] .= $char; } } private function commentDashState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); /* U+002D HYPHEN-MINUS (-) */ if ($char === '-') { /* Switch to the comment end state */ $this->state = 'commentEnd'; /* EOF */ } elseif ($this->char === $this->EOF) { /* Parse error. Emit the comment token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; /* Anything else */ } else { /* Append a U+002D HYPHEN-MINUS (-) character and the input character to the comment token's data. Switch to the comment state. */ $this->token['data'] .= '-' . $char; $this->state = 'comment'; } } private function commentEndState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '-') { $this->token['data'] .= '-'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['data'] .= '--' . $char; $this->state = 'comment'; } } private function doctypeState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { $this->state = 'beforeDoctypeName'; } else { $this->char--; $this->state = 'beforeDoctypeName'; } } private function beforeDoctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { // Stay in the before DOCTYPE name state. } elseif (preg_match('/^[a-z]$/', $char)) { $this->token = array( 'name' => strtoupper($char), 'type' => self::DOCTYPE, 'error' => true ); $this->state = 'doctypeName'; } elseif ($char === '>') { $this->emitToken( array( 'name' => null, 'type' => self::DOCTYPE, 'error' => true ) ); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken( array( 'name' => null, 'type' => self::DOCTYPE, 'error' => true ) ); $this->char--; $this->state = 'data'; } else { $this->token = array( 'name' => $char, 'type' => self::DOCTYPE, 'error' => true ); $this->state = 'doctypeName'; } } private function doctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { $this->state = 'AfterDoctypeName'; } elseif ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif (preg_match('/^[a-z]$/', $char)) { $this->token['name'] .= strtoupper($char); } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['name'] .= $char; } $this->token['error'] = ($this->token['name'] === 'HTML') ? false : true; } private function afterDoctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { // Stay in the DOCTYPE name state. } elseif ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['error'] = true; $this->state = 'bogusDoctype'; } } private function bogusDoctypeState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { // Stay in the bogus DOCTYPE state. } } private function entity() { $start = $this->char; // This section defines how to consume an entity. This definition is // used when parsing entities in text and in attributes. // The behaviour depends on the identity of the next character (the // one immediately after the U+0026 AMPERSAND character): switch ($this->character($this->char + 1)) { // U+0023 NUMBER SIGN (#) case '#': // The behaviour further depends on the character after the // U+0023 NUMBER SIGN: switch ($this->character($this->char + 1)) { // U+0078 LATIN SMALL LETTER X // U+0058 LATIN CAPITAL LETTER X case 'x': case 'X': // Follow the steps below, but using the range of // characters U+0030 DIGIT ZERO through to U+0039 DIGIT // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER // A, through to U+0046 LATIN CAPITAL LETTER F (in other // words, 0-9, A-F, a-f). $char = 1; $char_class = '0-9A-Fa-f'; break; // Anything else default: // Follow the steps below, but using the range of // characters U+0030 DIGIT ZERO through to U+0039 DIGIT // NINE (i.e. just 0-9). $char = 0; $char_class = '0-9'; break; } // Consume as many characters as match the range of characters // given above. $this->char++; $e_name = $this->characters($char_class, $this->char + $char + 1); $entity = $this->character($start, $this->char); $cond = strlen($e_name) > 0; // The rest of the parsing happens below. break; // Anything else default: // Consume the maximum number of characters possible, with the // consumed characters case-sensitively matching one of the // identifiers in the first column of the entities table. $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); $len = strlen($e_name); for ($c = 1; $c <= $len; $c++) { $id = substr($e_name, 0, $c); $this->char++; if (in_array($id, $this->entities)) { if ($e_name[$c - 1] !== ';') { if ($c < $len && $e_name[$c] == ';') { $this->char++; // consume extra semicolon } } $entity = $id; break; } } $cond = isset($entity); // The rest of the parsing happens below. break; } if (!$cond) { // If no match can be made, then this is a parse error. No // characters are consumed, and nothing is returned. $this->char = $start; return false; } // Return a character token for the character corresponding to the // entity name (as given by the second column of the entities table). return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8'); } private function emitToken($token) { $emit = $this->tree->emitToken($token); if (is_int($emit)) { $this->content_model = $emit; } elseif ($token['type'] === self::ENDTAG) { $this->content_model = self::PCDATA; } } private function EOF() { $this->state = null; $this->tree->emitToken( array( 'type' => self::EOF ) ); } } class HTML5TreeConstructer { public $stack = array(); private $phase; private $mode; private $dom; private $foster_parent = null; private $a_formatting = array(); private $head_pointer = null; private $form_pointer = null; private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); private $formatting = array( 'a', 'b', 'big', 'em', 'font', 'i', 'nobr', 's', 'small', 'strike', 'strong', 'tt', 'u' ); private $special = array( 'address', 'area', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'center', 'col', 'colgroup', 'dd', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'iframe', 'image', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'menu', 'meta', 'noembed', 'noframes', 'noscript', 'ol', 'optgroup', 'option', 'p', 'param', 'plaintext', 'pre', 'script', 'select', 'spacer', 'style', 'tbody', 'textarea', 'tfoot', 'thead', 'title', 'tr', 'ul', 'wbr' ); // The different phases. const INIT_PHASE = 0; const ROOT_PHASE = 1; const MAIN_PHASE = 2; const END_PHASE = 3; // The different insertion modes for the main phase. const BEFOR_HEAD = 0; const IN_HEAD = 1; const AFTER_HEAD = 2; const IN_BODY = 3; const IN_TABLE = 4; const IN_CAPTION = 5; const IN_CGROUP = 6; const IN_TBODY = 7; const IN_ROW = 8; const IN_CELL = 9; const IN_SELECT = 10; const AFTER_BODY = 11; const IN_FRAME = 12; const AFTR_FRAME = 13; // The different types of elements. const SPECIAL = 0; const SCOPING = 1; const FORMATTING = 2; const PHRASING = 3; const MARKER = 0; public function __construct() { $this->phase = self::INIT_PHASE; $this->mode = self::BEFOR_HEAD; $this->dom = new DOMDocument; $this->dom->encoding = 'UTF-8'; $this->dom->preserveWhiteSpace = true; $this->dom->substituteEntities = true; $this->dom->strictErrorChecking = false; } // Process tag tokens public function emitToken($token) { switch ($this->phase) { case self::INIT_PHASE: return $this->initPhase($token); break; case self::ROOT_PHASE: return $this->rootElementPhase($token); break; case self::MAIN_PHASE: return $this->mainPhase($token); break; case self::END_PHASE : return $this->trailingEndPhase($token); break; } } private function initPhase($token) { /* Initially, the tree construction stage must handle each token emitted from the tokenisation stage as follows: */ /* A DOCTYPE token that is marked as being in error A comment token A start tag token An end tag token A character token that is not one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE An end-of-file token */ if ((isset($token['error']) && $token['error']) || $token['type'] === HTML5::COMMENT || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG || $token['type'] === HTML5::EOF || ($token['type'] === HTML5::CHARACTR && isset($token['data']) && !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ) { /* This specification does not define how to handle this case. In particular, user agents may ignore the entirety of this specification altogether for such documents, and instead invoke special parse modes with a greater emphasis on backwards compatibility. */ $this->phase = self::ROOT_PHASE; return $this->rootElementPhase($token); /* A DOCTYPE token marked as being correct */ } elseif (isset($token['error']) && !$token['error']) { /* Append a DocumentType node to the Document node, with the name attribute set to the name given in the DOCTYPE token (which will be "HTML"), and the other attributes specific to DocumentType objects set to null, empty lists, or the empty string as appropriate. */ $doctype = new DOMDocumentType(null, null, 'HTML'); /* Then, switch to the root element phase of the tree construction stage. */ $this->phase = self::ROOT_PHASE; /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif (isset($token['data']) && preg_match( '/^[\t\n\x0b\x0c ]+$/', $token['data'] ) ) { /* Append that character to the Document node. */ $text = $this->dom->createTextNode($token['data']); $this->dom->appendChild($text); } } private function rootElementPhase($token) { /* After the initial phase, as each token is emitted from the tokenisation stage, it must be processed as described in this section. */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the Document object with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->dom->appendChild($comment); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append that character to the Document node. */ $text = $this->dom->createTextNode($token['data']); $this->dom->appendChild($text); /* A character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE A start tag token An end tag token An end-of-file token */ } elseif (($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG || $token['type'] === HTML5::EOF ) { /* Create an HTMLElement node with the tag name html, in the HTML namespace. Append it to the Document object. Switch to the main phase and reprocess the current token. */ $html = $this->dom->createElement('html'); $this->dom->appendChild($html); $this->stack[] = $html; $this->phase = self::MAIN_PHASE; return $this->mainPhase($token); } } private function mainPhase($token) { /* Tokens in the main phase must be handled as follows: */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A start tag token with the tag name "html" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { /* If this start tag token was not the first start tag token, then it is a parse error. */ /* For each attribute on the token, check to see if the attribute is already present on the top element of the stack of open elements. If it is not, add the attribute and its corresponding value to that element. */ foreach ($token['attr'] as $attr) { if (!$this->stack[0]->hasAttribute($attr['name'])) { $this->stack[0]->setAttribute($attr['name'], $attr['value']); } } /* An end-of-file token */ } elseif ($token['type'] === HTML5::EOF) { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Anything else. */ } else { /* Depends on the insertion mode: */ switch ($this->mode) { case self::BEFOR_HEAD: return $this->beforeHead($token); break; case self::IN_HEAD: return $this->inHead($token); break; case self::AFTER_HEAD: return $this->afterHead($token); break; case self::IN_BODY: return $this->inBody($token); break; case self::IN_TABLE: return $this->inTable($token); break; case self::IN_CAPTION: return $this->inCaption($token); break; case self::IN_CGROUP: return $this->inColumnGroup($token); break; case self::IN_TBODY: return $this->inTableBody($token); break; case self::IN_ROW: return $this->inRow($token); break; case self::IN_CELL: return $this->inCell($token); break; case self::IN_SELECT: return $this->inSelect($token); break; case self::AFTER_BODY: return $this->afterBody($token); break; case self::IN_FRAME: return $this->inFrameset($token); break; case self::AFTR_FRAME: return $this->afterFrameset($token); break; case self::END_PHASE: return $this->trailingEndPhase($token); break; } } } private function beforeHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token with the tag name "head" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { /* Create an element for the token, append the new element to the current node and push it onto the stack of open elements. */ $element = $this->insertElement($token); /* Set the head element pointer to this new element node. */ $this->head_pointer = $element; /* Change the insertion mode to "in head". */ $this->mode = self::IN_HEAD; /* A start tag token whose tag name is one of: "base", "link", "meta", "script", "style", "title". Or an end tag with the tag name "html". Or a character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. Or any other start tag token */ } elseif ($token['type'] === HTML5::STARTTAG || ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || ($token['type'] === HTML5::CHARACTR && !preg_match( '/^[\t\n\x0b\x0c ]$/', $token['data'] )) ) { /* Act as if a start tag token with the tag name "head" and no attributes had been seen, then reprocess the current token. */ $this->beforeHead( array( 'name' => 'head', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inHead($token); /* Any other end tag */ } elseif ($token['type'] === HTML5::ENDTAG) { /* Parse error. Ignore the token. */ } } private function inHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. THIS DIFFERS FROM THE SPEC: If the current node is either a title, style or script element, append the character to the current node regardless of its content. */ if (($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( $token['type'] === HTML5::CHARACTR && in_array( end($this->stack)->nodeName, array('title', 'style', 'script') )) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('title', 'style', 'script')) ) { array_pop($this->stack); return HTML5::PCDATA; /* A start tag with the tag name "title" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); } else { $element = $this->insertElement($token); } /* Switch the tokeniser's content model flag to the RCDATA state. */ return HTML5::RCDATA; /* A start tag with the tag name "style" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); } else { $this->insertElement($token); } /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; /* A start tag with the tag name "script" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { /* Create an element for the token. */ $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; /* A start tag with the tag name "base", "link", or "meta" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('base', 'link', 'meta') ) ) { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); array_pop($this->stack); } else { $this->insertElement($token); } /* An end tag with the tag name "head" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { /* If the current node is a head element, pop the current node off the stack of open elements. */ if ($this->head_pointer->isSameNode(end($this->stack))) { array_pop($this->stack); /* Otherwise, this is a parse error. */ } else { // k } /* Change the insertion mode to "after head". */ $this->mode = self::AFTER_HEAD; /* A start tag with the tag name "head" or an end tag except "html". */ } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') ) { // Parse error. Ignore the token. /* Anything else */ } else { /* If the current node is a head element, act as if an end tag token with the tag name "head" had been seen. */ if ($this->head_pointer->isSameNode(end($this->stack))) { $this->inHead( array( 'name' => 'head', 'type' => HTML5::ENDTAG ) ); /* Otherwise, change the insertion mode to "after head". */ } else { $this->mode = self::AFTER_HEAD; } /* Then, reprocess the current token. */ return $this->afterHead($token); } } private function afterHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token with the tag name "body" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { /* Insert a body element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in body". */ $this->mode = self::IN_BODY; /* A start tag token with the tag name "frameset" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { /* Insert a frameset element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in frameset". */ $this->mode = self::IN_FRAME; /* A start tag token whose tag name is one of: "base", "link", "meta", "script", "style", "title" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('base', 'link', 'meta', 'script', 'style', 'title') ) ) { /* Parse error. Switch the insertion mode back to "in head" and reprocess the token. */ $this->mode = self::IN_HEAD; return $this->inHead($token); /* Anything else */ } else { /* Act as if a start tag token with the tag name "body" and no attributes had been seen, and then reprocess the current token. */ $this->afterHead( array( 'name' => 'body', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inBody($token); } } private function inBody($token) { /* Handle the token as follows: */ switch ($token['type']) { /* A character token */ case HTML5::CHARACTR: /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Append the token's character to the current node. */ $this->insertText($token['data']); break; /* A comment token */ case HTML5::COMMENT: /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); break; case HTML5::STARTTAG: switch ($token['name']) { /* A start tag token whose tag name is one of: "script", "style" */ case 'script': case 'style': /* Process the token as if the insertion mode had been "in head". */ return $this->inHead($token); break; /* A start tag token whose tag name is one of: "base", "link", "meta", "title" */ case 'base': case 'link': case 'meta': case 'title': /* Parse error. Process the token as if the insertion mode had been "in head". */ return $this->inHead($token); break; /* A start tag token with the tag name "body" */ case 'body': /* Parse error. If the second element on the stack of open elements is not a body element, or, if the stack of open elements has only one node on it, then ignore the token. (innerHTML case) */ if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { // Ignore /* Otherwise, for each attribute on the token, check to see if the attribute is already present on the body element (the second element) on the stack of open elements. If it is not, add the attribute and its corresponding value to that element. */ } else { foreach ($token['attr'] as $attr) { if (!$this->stack[1]->hasAttribute($attr['name'])) { $this->stack[1]->setAttribute($attr['name'], $attr['value']); } } } break; /* A start tag whose tag name is one of: "address", "blockquote", "center", "dir", "div", "dl", "fieldset", "listing", "menu", "ol", "p", "ul" */ case 'address': case 'blockquote': case 'center': case 'dir': case 'div': case 'dl': case 'fieldset': case 'listing': case 'menu': case 'ol': case 'p': case 'ul': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); break; /* A start tag whose tag name is "form" */ case 'form': /* If the form element pointer is not null, ignore the token with a parse error. */ if ($this->form_pointer !== null) { // Ignore. /* Otherwise: */ } else { /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token, and set the form element pointer to point to the element created. */ $element = $this->insertElement($token); $this->form_pointer = $element; } break; /* A start tag whose tag name is "li", "dd" or "dt" */ case 'li': case 'dd': case 'dt': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } $stack_length = count($this->stack) - 1; for ($n = $stack_length; 0 <= $n; $n--) { /* 1. Initialise node to be the current node (the bottommost node of the stack). */ $stop = false; $node = $this->stack[$n]; $cat = $this->getElementCategory($node->tagName); /* 2. If node is an li, dd or dt element, then pop all the nodes from the current node up to node, including node, then stop this algorithm. */ if ($token['name'] === $node->tagName || ($token['name'] !== 'li' && ($node->tagName === 'dd' || $node->tagName === 'dt')) ) { for ($x = $stack_length; $x >= $n; $x--) { array_pop($this->stack); } break; } /* 3. If node is not in the formatting category, and is not in the phrasing category, and is not an address or div element, then stop this algorithm. */ if ($cat !== self::FORMATTING && $cat !== self::PHRASING && $node->tagName !== 'address' && $node->tagName !== 'div' ) { break; } } /* Finally, insert an HTML element with the same tag name as the token's. */ $this->insertElement($token); break; /* A start tag token whose tag name is "plaintext" */ case 'plaintext': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); return HTML5::PLAINTEXT; break; /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" */ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a parse error; pop elements from the stack until an element with one of those tag names has been popped from the stack. */ while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { array_pop($this->stack); } /* Insert an HTML element for the token. */ $this->insertElement($token); break; /* A start tag whose tag name is "a" */ case 'a': /* If the list of active formatting elements contains an element whose tag name is "a" between the end of the list and the last marker on the list (or the start of the list if there is no marker on the list), then this is a parse error; act as if an end tag with the tag name "a" had been seen, then remove that element from the list of active formatting elements and the stack of open elements if the end tag didn't already remove it (it might not have if the element is not in table scope). */ $leng = count($this->a_formatting); for ($n = $leng - 1; $n >= 0; $n--) { if ($this->a_formatting[$n] === self::MARKER) { break; } elseif ($this->a_formatting[$n]->nodeName === 'a') { $this->emitToken( array( 'name' => 'a', 'type' => HTML5::ENDTAG ) ); break; } } /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $el = $this->insertElement($token); /* Add that element to the list of active formatting elements. */ $this->a_formatting[] = $el; break; /* A start tag whose tag name is one of: "b", "big", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ case 'b': case 'big': case 'em': case 'font': case 'i': case 'nobr': case 's': case 'small': case 'strike': case 'strong': case 'tt': case 'u': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $el = $this->insertElement($token); /* Add that element to the list of active formatting elements. */ $this->a_formatting[] = $el; break; /* A start tag token whose tag name is "button" */ case 'button': /* If the stack of open elements has a button element in scope, then this is a parse error; act as if an end tag with the tag name "button" had been seen, then reprocess the token. (We don't do that. Unnecessary.) */ if ($this->elementInScope('button')) { $this->inBody( array( 'name' => 'button', 'type' => HTML5::ENDTAG ) ); } /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; break; /* A start tag token whose tag name is one of: "marquee", "object" */ case 'marquee': case 'object': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; break; /* A start tag token whose tag name is "xmp" */ case 'xmp': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Switch the content model flag to the CDATA state. */ return HTML5::CDATA; break; /* A start tag whose tag name is "table" */ case 'table': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in table". */ $this->mode = self::IN_TABLE; break; /* A start tag whose tag name is one of: "area", "basefont", "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ case 'area': case 'basefont': case 'bgsound': case 'br': case 'embed': case 'img': case 'param': case 'spacer': case 'wbr': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "hr" */ case 'hr': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "image" */ case 'image': /* Parse error. Change the token's tag name to "img" and reprocess it. (Don't ask.) */ $token['name'] = 'img'; return $this->inBody($token); break; /* A start tag whose tag name is "input" */ case 'input': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an input element for the token. */ $element = $this->insertElement($token, false); /* If the form element pointer is not null, then associate the input element with the form element pointed to by the form element pointer. */ $this->form_pointer !== null ? $this->form_pointer->appendChild($element) : end($this->stack)->appendChild($element); /* Pop that input element off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "isindex" */ case 'isindex': /* Parse error. */ // w/e /* If the form element pointer is not null, then ignore the token. */ if ($this->form_pointer === null) { /* Act as if a start tag token with the tag name "form" had been seen. */ $this->inBody( array( 'name' => 'body', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "hr" had been seen. */ $this->inBody( array( 'name' => 'hr', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "p" had been seen. */ $this->inBody( array( 'name' => 'p', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "label" had been seen. */ $this->inBody( array( 'name' => 'label', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a stream of character tokens had been seen. */ $this->insertText( 'This is a searchable index. ' . 'Insert your search keywords here: ' ); /* Act as if a start tag token with the tag name "input" had been seen, with all the attributes from the "isindex" token, except with the "name" attribute set to the value "isindex" (ignoring any explicit "name" attribute). */ $attr = $token['attr']; $attr[] = array('name' => 'name', 'value' => 'isindex'); $this->inBody( array( 'name' => 'input', 'type' => HTML5::STARTTAG, 'attr' => $attr ) ); /* Act as if a stream of character tokens had been seen (see below for what they should say). */ $this->insertText( 'This is a searchable index. ' . 'Insert your search keywords here: ' ); /* Act as if an end tag token with the tag name "label" had been seen. */ $this->inBody( array( 'name' => 'label', 'type' => HTML5::ENDTAG ) ); /* Act as if an end tag token with the tag name "p" had been seen. */ $this->inBody( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); /* Act as if a start tag token with the tag name "hr" had been seen. */ $this->inBody( array( 'name' => 'hr', 'type' => HTML5::ENDTAG ) ); /* Act as if an end tag token with the tag name "form" had been seen. */ $this->inBody( array( 'name' => 'form', 'type' => HTML5::ENDTAG ) ); } break; /* A start tag whose tag name is "textarea" */ case 'textarea': $this->insertElement($token); /* Switch the tokeniser's content model flag to the RCDATA state. */ return HTML5::RCDATA; break; /* A start tag whose tag name is one of: "iframe", "noembed", "noframes" */ case 'iframe': case 'noembed': case 'noframes': $this->insertElement($token); /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; break; /* A start tag whose tag name is "select" */ case 'select': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in select". */ $this->mode = self::IN_SELECT; break; /* A start or end tag whose tag name is one of: "caption", "col", "colgroup", "frame", "frameset", "head", "option", "optgroup", "tbody", "td", "tfoot", "th", "thead", "tr". */ case 'caption': case 'col': case 'colgroup': case 'frame': case 'frameset': case 'head': case 'option': case 'optgroup': case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr': // Parse error. Ignore the token. break; /* A start or end tag whose tag name is one of: "event-source", "section", "nav", "article", "aside", "header", "footer", "datagrid", "command" */ case 'event-source': case 'section': case 'nav': case 'article': case 'aside': case 'header': case 'footer': case 'datagrid': case 'command': // Work in progress! break; /* A start tag token not covered by the previous entries */ default: /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); $this->insertElement($token, true, true); break; } break; case HTML5::ENDTAG: switch ($token['name']) { /* An end tag with the tag name "body" */ case 'body': /* If the second element in the stack of open elements is not a body element, this is a parse error. Ignore the token. (innerHTML case) */ if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { // Ignore. /* If the current node is not the body element, then this is a parse error. */ } elseif (end($this->stack)->nodeName !== 'body') { // Parse error. } /* Change the insertion mode to "after body". */ $this->mode = self::AFTER_BODY; break; /* An end tag with the tag name "html" */ case 'html': /* Act as if an end tag with tag name "body" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inBody( array( 'name' => 'body', 'type' => HTML5::ENDTAG ) ); return $this->afterBody($token); break; /* An end tag whose tag name is one of: "address", "blockquote", "center", "dir", "div", "dl", "fieldset", "listing", "menu", "ol", "pre", "ul" */ case 'address': case 'blockquote': case 'center': case 'dir': case 'div': case 'dl': case 'fieldset': case 'listing': case 'menu': case 'ol': case 'pre': case 'ul': /* If the stack of open elements has an element in scope with the same tag name as that of the token, then generate implied end tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e /* If the stack of open elements has an element in scope with the same tag name as that of the token, then pop elements from this stack until an element with that tag name has been popped from the stack. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } } break; /* An end tag whose tag name is "form" */ case 'form': /* If the stack of open elements has an element in scope with the same tag name as that of the token, then generate implied end tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); } if (end($this->stack)->nodeName !== $token['name']) { /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e } else { /* Otherwise, if the current node is an element with the same tag name as that of the token pop that element from the stack. */ array_pop($this->stack); } /* In any case, set the form element pointer to null. */ $this->form_pointer = null; break; /* An end tag whose tag name is "p" */ case 'p': /* If the stack of open elements has a p element in scope, then generate implied end tags, except for p elements. */ if ($this->elementInScope('p')) { $this->generateImpliedEndTags(array('p')); /* If the current node is not a p element, then this is a parse error. */ // k /* If the stack of open elements has a p element in scope, then pop elements from this stack until the stack no longer has a p element in scope. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->elementInScope('p')) { array_pop($this->stack); } else { break; } } } break; /* An end tag whose tag name is "dd", "dt", or "li" */ case 'dd': case 'dt': case 'li': /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then generate implied end tags, except for elements with the same tag name as the token. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(array($token['name'])); /* If the current node is not an element with the same tag name as the token, then this is a parse error. */ // w/e /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then pop elements from this stack until an element with that tag name has been popped from the stack. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } } break; /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" */ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then generate implied end tags. */ if ($this->elementInScope($elements)) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then pop elements from the stack until an element with one of those tag names has been popped from the stack. */ while ($this->elementInScope($elements)) { array_pop($this->stack); } } break; /* An end tag whose tag name is one of: "a", "b", "big", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ case 'a': case 'b': case 'big': case 'em': case 'font': case 'i': case 'nobr': case 's': case 'small': case 'strike': case 'strong': case 'tt': case 'u': /* 1. Let the formatting element be the last element in the list of active formatting elements that: * is between the end of the list and the last scope marker in the list, if any, or the start of the list otherwise, and * has the same tag name as the token. */ while (true) { for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { if ($this->a_formatting[$a] === self::MARKER) { break; } elseif ($this->a_formatting[$a]->tagName === $token['name']) { $formatting_element = $this->a_formatting[$a]; $in_stack = in_array($formatting_element, $this->stack, true); $fe_af_pos = $a; break; } } /* If there is no such node, or, if that node is also in the stack of open elements but the element is not in scope, then this is a parse error. Abort these steps. The token is ignored. */ if (!isset($formatting_element) || ($in_stack && !$this->elementInScope($token['name'])) ) { break; /* Otherwise, if there is such a node, but that node is not in the stack of open elements, then this is a parse error; remove the element from the list, and abort these steps. */ } elseif (isset($formatting_element) && !$in_stack) { unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); break; } /* 2. Let the furthest block be the topmost node in the stack of open elements that is lower in the stack than the formatting element, and is not an element in the phrasing or formatting categories. There might not be one. */ $fe_s_pos = array_search($formatting_element, $this->stack, true); $length = count($this->stack); for ($s = $fe_s_pos + 1; $s < $length; $s++) { $category = $this->getElementCategory($this->stack[$s]->nodeName); if ($category !== self::PHRASING && $category !== self::FORMATTING) { $furthest_block = $this->stack[$s]; } } /* 3. If there is no furthest block, then the UA must skip the subsequent steps and instead just pop all the nodes from the bottom of the stack of open elements, from the current node up to the formatting element, and remove the formatting element from the list of active formatting elements. */ if (!isset($furthest_block)) { for ($n = $length - 1; $n >= $fe_s_pos; $n--) { array_pop($this->stack); } unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); break; } /* 4. Let the common ancestor be the element immediately above the formatting element in the stack of open elements. */ $common_ancestor = $this->stack[$fe_s_pos - 1]; /* 5. If the furthest block has a parent node, then remove the furthest block from its parent node. */ if ($furthest_block->parentNode !== null) { $furthest_block->parentNode->removeChild($furthest_block); } /* 6. Let a bookmark note the position of the formatting element in the list of active formatting elements relative to the elements on either side of it in the list. */ $bookmark = $fe_af_pos; /* 7. Let node and last node be the furthest block. Follow these steps: */ $node = $furthest_block; $last_node = $furthest_block; while (true) { for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { /* 7.1 Let node be the element immediately prior to node in the stack of open elements. */ $node = $this->stack[$n]; /* 7.2 If node is not in the list of active formatting elements, then remove node from the stack of open elements and then go back to step 1. */ if (!in_array($node, $this->a_formatting, true)) { unset($this->stack[$n]); $this->stack = array_merge($this->stack); } else { break; } } /* 7.3 Otherwise, if node is the formatting element, then go to the next step in the overall algorithm. */ if ($node === $formatting_element) { break; /* 7.4 Otherwise, if last node is the furthest block, then move the aforementioned bookmark to be immediately after the node in the list of active formatting elements. */ } elseif ($last_node === $furthest_block) { $bookmark = array_search($node, $this->a_formatting, true) + 1; } /* 7.5 If node has any children, perform a shallow clone of node, replace the entry for node in the list of active formatting elements with an entry for the clone, replace the entry for node in the stack of open elements with an entry for the clone, and let node be the clone. */ if ($node->hasChildNodes()) { $clone = $node->cloneNode(); $s_pos = array_search($node, $this->stack, true); $a_pos = array_search($node, $this->a_formatting, true); $this->stack[$s_pos] = $clone; $this->a_formatting[$a_pos] = $clone; $node = $clone; } /* 7.6 Insert last node into node, first removing it from its previous parent node if any. */ if ($last_node->parentNode !== null) { $last_node->parentNode->removeChild($last_node); } $node->appendChild($last_node); /* 7.7 Let last node be node. */ $last_node = $node; } /* 8. Insert whatever last node ended up being in the previous step into the common ancestor node, first removing it from its previous parent node if any. */ if ($last_node->parentNode !== null) { $last_node->parentNode->removeChild($last_node); } $common_ancestor->appendChild($last_node); /* 9. Perform a shallow clone of the formatting element. */ $clone = $formatting_element->cloneNode(); /* 10. Take all of the child nodes of the furthest block and append them to the clone created in the last step. */ while ($furthest_block->hasChildNodes()) { $child = $furthest_block->firstChild; $furthest_block->removeChild($child); $clone->appendChild($child); } /* 11. Append that clone to the furthest block. */ $furthest_block->appendChild($clone); /* 12. Remove the formatting element from the list of active formatting elements, and insert the clone into the list of active formatting elements at the position of the aforementioned bookmark. */ $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); /* 13. Remove the formatting element from the stack of open elements, and insert the clone into the stack of open elements immediately after (i.e. in a more deeply nested position than) the position of the furthest block in that stack. */ $fe_s_pos = array_search($formatting_element, $this->stack, true); $fb_s_pos = array_search($furthest_block, $this->stack, true); unset($this->stack[$fe_s_pos]); $s_part1 = array_slice($this->stack, 0, $fb_s_pos); $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); $this->stack = array_merge($s_part1, array($clone), $s_part2); /* 14. Jump back to step 1 in this series of steps. */ unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); } break; /* An end tag token whose tag name is one of: "button", "marquee", "object" */ case 'button': case 'marquee': case 'object': /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then generate implied tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as the token, then this is a parse error. */ // k /* Now, if the stack of open elements has an element in scope whose tag name matches the tag name of the token, then pop elements from the stack until that element has been popped from the stack, and clear the list of active formatting elements up to the last marker. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } $marker = end(array_keys($this->a_formatting, self::MARKER, true)); for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { array_pop($this->a_formatting); } } break; /* Or an end tag whose tag name is one of: "area", "basefont", "bgsound", "br", "embed", "hr", "iframe", "image", "img", "input", "isindex", "noembed", "noframes", "param", "select", "spacer", "table", "textarea", "wbr" */ case 'area': case 'basefont': case 'bgsound': case 'br': case 'embed': case 'hr': case 'iframe': case 'image': case 'img': case 'input': case 'isindex': case 'noembed': case 'noframes': case 'param': case 'select': case 'spacer': case 'table': case 'textarea': case 'wbr': // Parse error. Ignore the token. break; /* An end tag token not covered by the previous entries */ default: for ($n = count($this->stack) - 1; $n >= 0; $n--) { /* Initialise node to be the current node (the bottommost node of the stack). */ $node = end($this->stack); /* If node has the same tag name as the end tag token, then: */ if ($token['name'] === $node->nodeName) { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* If the tag name of the end tag token does not match the tag name of the current node, this is a parse error. */ // k /* Pop all the nodes from the current node up to node, including node, then stop this algorithm. */ for ($x = count($this->stack) - $n; $x >= $n; $x--) { array_pop($this->stack); } } else { $category = $this->getElementCategory($node); if ($category !== self::SPECIAL && $category !== self::SCOPING) { /* Otherwise, if node is in neither the formatting category nor the phrasing category, then this is a parse error. Stop this algorithm. The end tag token is ignored. */ return false; } } } break; } break; } } private function inTable($token) { $clear = array('html', 'table'); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $text = $this->dom->createTextNode($token['data']); end($this->stack)->appendChild($text); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); end($this->stack)->appendChild($comment); /* A start tag whose tag name is "caption" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'caption' ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; /* Insert an HTML element for the token, then switch the insertion mode to "in caption". */ $this->insertElement($token); $this->mode = self::IN_CAPTION; /* A start tag whose tag name is "colgroup" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'colgroup' ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in column group". */ $this->insertElement($token); $this->mode = self::IN_CGROUP; /* A start tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col' ) { $this->inTable( array( 'name' => 'colgroup', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); $this->inColumnGroup($token); /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('tbody', 'tfoot', 'thead') ) ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in table body". */ $this->insertElement($token); $this->mode = self::IN_TBODY; /* A start tag whose tag name is one of: "td", "th", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array($token['name'], array('td', 'th', 'tr')) ) { /* Act as if a start tag token with the tag name "tbody" had been seen, then reprocess the current token. */ $this->inTable( array( 'name' => 'tbody', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inTableBody($token); /* A start tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table' ) { /* Parse error. Act as if an end tag token with the tag name "table" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inTable( array( 'name' => 'table', 'type' => HTML5::ENDTAG ) ); return $this->mainPhase($token); /* An end tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'table' ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { return false; /* Otherwise: */ } else { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Now, if the current node is not a table element, then this is a parse error. */ // w/e /* Pop elements from this stack until a table element has been popped from the stack. */ while (true) { $current = end($this->stack)->nodeName; array_pop($this->stack); if ($current === 'table') { break; } } /* Reset the insertion mode appropriately. */ $this->resetInsertionMode(); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array( 'body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { // Parse error. Ignore the token. /* Anything else */ } else { /* Parse error. Process the token as if the insertion mode was "in body", with the following exception: */ /* If the current node is a table, tbody, tfoot, thead, or tr element, then, whenever a node would be inserted into the current node, it must instead be inserted into the foster parent element. */ if (in_array( end($this->stack)->nodeName, array('table', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* The foster parent element is the parent element of the last table element in the stack of open elements, if there is a table element and it has such a parent element. If there is no table element in the stack of open elements (innerHTML case), then the foster parent element is the first element in the stack of open elements (the html element). Otherwise, if there is a table element in the stack of open elements, but the last table element in the stack of open elements has no parent, or its parent node is not an element, then the foster parent element is the element before the last table element in the stack of open elements. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === 'table') { $table = $this->stack[$n]; break; } } if (isset($table) && $table->parentNode !== null) { $this->foster_parent = $table->parentNode; } elseif (!isset($table)) { $this->foster_parent = $this->stack[0]; } elseif (isset($table) && ($table->parentNode === null || $table->parentNode->nodeType !== XML_ELEMENT_NODE) ) { $this->foster_parent = $this->stack[$n - 1]; } } $this->inBody($token); } } private function inCaption($token) { /* An end tag whose tag name is "caption" */ if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // Ignore /* Otherwise: */ } else { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Now, if the current node is not a caption element, then this is a parse error. */ // w/e /* Pop elements from this stack until a caption element has been popped from the stack. */ while (true) { $node = end($this->stack)->nodeName; array_pop($this->stack); if ($node === 'caption') { break; } } /* Clear the list of active formatting elements up to the last marker. */ $this->clearTheActiveFormattingElementsUpToTheLastMarker(); /* Switch the insertion mode to "in table". */ $this->mode = self::IN_TABLE; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag name is "table" */ } elseif (($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) )) || ($token['type'] === HTML5::ENDTAG && $token['name'] === 'table') ) { /* Parse error. Act as if an end tag with the tag name "caption" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inCaption( array( 'name' => 'caption', 'type' => HTML5::ENDTAG ) ); return $this->inTable($token); /* An end tag whose tag name is one of: "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array( 'body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', 'thead', 'tr' ) ) ) { // Parse error. Ignore the token. /* Anything else */ } else { /* Process the token as if the insertion mode was "in body". */ $this->inBody($token); } } private function inColumnGroup($token) { /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $text = $this->dom->createTextNode($token['data']); end($this->stack)->appendChild($text); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); end($this->stack)->appendChild($comment); /* A start tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { /* Insert a col element for the token. Immediately pop the current node off the stack of open elements. */ $this->insertElement($token); array_pop($this->stack); /* An end tag whose tag name is "colgroup" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'colgroup' ) { /* If the current node is the root html element, then this is a parse error, ignore the token. (innerHTML case) */ if (end($this->stack)->nodeName === 'html') { // Ignore /* Otherwise, pop the current node (which will be a colgroup element) from the stack of open elements. Switch the insertion mode to "in table". */ } else { array_pop($this->stack); $this->mode = self::IN_TABLE; } /* An end tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Act as if an end tag with the tag name "colgroup" had been seen, and then, if that token wasn't ignored, reprocess the current token. */ $this->inColumnGroup( array( 'name' => 'colgroup', 'type' => HTML5::ENDTAG ) ); return $this->inTable($token); } } private function inTableBody($token) { $clear = array('tbody', 'tfoot', 'thead', 'html'); /* A start tag whose tag name is "tr" */ if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Insert a tr element for the token, then switch the insertion mode to "in row". */ $this->insertElement($token); $this->mode = self::IN_ROW; /* A start tag whose tag name is one of: "th", "td" */ } elseif ($token['type'] === HTML5::STARTTAG && ($token['name'] === 'th' || $token['name'] === 'td') ) { /* Parse error. Act as if a start tag with the tag name "tr" had been seen, then reprocess the current token. */ $this->inTableBody( array( 'name' => 'tr', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inRow($token); /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('tbody', 'tfoot', 'thead')) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. */ if (!$this->elementInScope($token['name'], true)) { // Ignore /* Otherwise: */ } else { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Pop the current node from the stack of open elements. Switch the insertion mode to "in table". */ array_pop($this->stack); $this->mode = self::IN_TABLE; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ } elseif (($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') )) || ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') ) { /* If the stack of open elements does not have a tbody, thead, or tfoot element in table scope, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { // Ignore. /* Otherwise: */ } else { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Act as if an end tag with the same tag name as the current node ("tbody", "tfoot", or "thead") had been seen, then reprocess the current token. */ $this->inTableBody( array( 'name' => end($this->stack)->nodeName, 'type' => HTML5::ENDTAG ) ); return $this->mainPhase($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') ) ) { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Process the token as if the insertion mode was "in table". */ $this->inTable($token); } } private function inRow($token) { $clear = array('tr', 'html'); /* A start tag whose tag name is one of: "th", "td" */ if ($token['type'] === HTML5::STARTTAG && ($token['name'] === 'th' || $token['name'] === 'td') ) { /* Clear the stack back to a table row context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in cell". */ $this->insertElement($token); $this->mode = self::IN_CELL; /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; /* An end tag whose tag name is "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Clear the stack back to a table row context. */ $this->clearStackToTableContext($clear); /* Pop the current node (which will be a tr element) from the stack of open elements. Switch the insertion mode to "in table body". */ array_pop($this->stack); $this->mode = self::IN_TBODY; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* Act as if an end tag with the tag name "tr" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inRow( array( 'name' => 'tr', 'type' => HTML5::ENDTAG ) ); return $this->inCell($token); /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('tbody', 'tfoot', 'thead')) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Otherwise, act as if an end tag with the tag name "tr" had been seen, then reprocess the current token. */ $this->inRow( array( 'name' => 'tr', 'type' => HTML5::ENDTAG ) ); return $this->inCell($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') ) ) { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Process the token as if the insertion mode was "in table". */ $this->inTable($token); } } private function inCell($token) { /* An end tag whose tag name is one of: "td", "th" */ if ($token['type'] === HTML5::ENDTAG && ($token['name'] === 'td' || $token['name'] === 'th') ) { /* If the stack of open elements does not have an element in table scope with the same tag name as that of the token, then this is a parse error and the token must be ignored. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Generate implied end tags, except for elements with the same tag name as the token. */ $this->generateImpliedEndTags(array($token['name'])); /* Now, if the current node is not an element with the same tag name as the token, then this is a parse error. */ // k /* Pop elements from this stack until an element with the same tag name as the token has been popped from the stack. */ while (true) { $node = end($this->stack)->nodeName; array_pop($this->stack); if ($node === $token['name']) { break; } } /* Clear the list of active formatting elements up to the last marker. */ $this->clearTheActiveFormattingElementsUpToTheLastMarker(); /* Switch the insertion mode to "in row". (The current node will be a tr element at this point.) */ $this->mode = self::IN_ROW; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { /* If the stack of open elements does not have a td or th element in table scope, then this is a parse error; ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('td', 'th'), true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { /* If the stack of open elements does not have a td or th element in table scope, then this is a parse error; ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('td', 'th'), true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html') ) ) { /* Parse error. Ignore the token. */ /* An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('table', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as that of the token (which can only happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), then this is a parse error and the token must be ignored. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* Anything else */ } else { /* Process the token as if the insertion mode was "in body". */ $this->inBody($token); } } private function inSelect($token) { /* Handle the token as follows: */ /* A character token */ if ($token['type'] === HTML5::CHARACTR) { /* Append the token's character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token whose tag name is "option" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'option' ) { /* If the current node is an option element, act as if an end tag with the tag name "option" had been seen. */ if (end($this->stack)->nodeName === 'option') { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* A start tag token whose tag name is "optgroup" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'optgroup' ) { /* If the current node is an option element, act as if an end tag with the tag name "option" had been seen. */ if (end($this->stack)->nodeName === 'option') { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* If the current node is an optgroup element, act as if an end tag with the tag name "optgroup" had been seen. */ if (end($this->stack)->nodeName === 'optgroup') { $this->inSelect( array( 'name' => 'optgroup', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* An end tag token whose tag name is "optgroup" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'optgroup' ) { /* First, if the current node is an option element, and the node immediately before it in the stack of open elements is an optgroup element, then act as if an end tag with the tag name "option" had been seen. */ $elements_in_stack = count($this->stack); if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' ) { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* If the current node is an optgroup element, then pop that node from the stack of open elements. Otherwise, this is a parse error, ignore the token. */ if ($this->stack[$elements_in_stack - 1] === 'optgroup') { array_pop($this->stack); } /* An end tag token whose tag name is "option" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'option' ) { /* If the current node is an option element, then pop that node from the stack of open elements. Otherwise, this is a parse error, ignore the token. */ if (end($this->stack)->nodeName === 'option') { array_pop($this->stack); } /* An end tag whose tag name is "select" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'select' ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // w/e /* Otherwise: */ } else { /* Pop elements from the stack of open elements until a select element has been popped from the stack. */ while (true) { $current = end($this->stack)->nodeName; array_pop($this->stack); if ($current === 'select') { break; } } /* Reset the insertion mode appropriately. */ $this->resetInsertionMode(); } /* A start tag whose tag name is "select" */ } elseif ($token['name'] === 'select' && $token['type'] === HTML5::STARTTAG ) { /* Parse error. Act as if the token had been an end tag with the tag name "select" instead. */ $this->inSelect( array( 'name' => 'select', 'type' => HTML5::ENDTAG ) ); /* An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" */ } elseif (in_array( $token['name'], array( 'caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th' ) ) && $token['type'] === HTML5::ENDTAG ) { /* Parse error. */ // w/e /* If the stack of open elements has an element in table scope with the same tag name as that of the token, then act as if an end tag with the tag name "select" had been seen, and reprocess the token. Otherwise, ignore the token. */ if ($this->elementInScope($token['name'], true)) { $this->inSelect( array( 'name' => 'select', 'type' => HTML5::ENDTAG ) ); $this->mainPhase($token); } /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function afterBody($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Process the token as it would be processed if the insertion mode was "in body". */ $this->inBody($token); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the first element in the stack of open elements (the html element), with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->stack[0]->appendChild($comment); /* An end tag with the tag name "html" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { /* If the parser was originally created in order to handle the setting of an element's innerHTML attribute, this is a parse error; ignore the token. (The element will be an html element in this case.) (innerHTML case) */ /* Otherwise, switch to the trailing end phase. */ $this->phase = self::END_PHASE; /* Anything else */ } else { /* Parse error. Set the insertion mode to "in body" and reprocess the token. */ $this->mode = self::IN_BODY; return $this->inBody($token); } } private function inFrameset($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag with the tag name "frameset" */ } elseif ($token['name'] === 'frameset' && $token['type'] === HTML5::STARTTAG ) { $this->insertElement($token); /* An end tag with the tag name "frameset" */ } elseif ($token['name'] === 'frameset' && $token['type'] === HTML5::ENDTAG ) { /* If the current node is the root html element, then this is a parse error; ignore the token. (innerHTML case) */ if (end($this->stack)->nodeName === 'html') { // Ignore } else { /* Otherwise, pop the current node from the stack of open elements. */ array_pop($this->stack); /* If the parser was not originally created in order to handle the setting of an element's innerHTML attribute (innerHTML case), and the current node is no longer a frameset element, then change the insertion mode to "after frameset". */ $this->mode = self::AFTR_FRAME; } /* A start tag with the tag name "frame" */ } elseif ($token['name'] === 'frame' && $token['type'] === HTML5::STARTTAG ) { /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); /* A start tag with the tag name "noframes" */ } elseif ($token['name'] === 'noframes' && $token['type'] === HTML5::STARTTAG ) { /* Process the token as if the insertion mode had been "in body". */ $this->inBody($token); /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function afterFrameset($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* An end tag with the tag name "html" */ } elseif ($token['name'] === 'html' && $token['type'] === HTML5::ENDTAG ) { /* Switch to the trailing end phase. */ $this->phase = self::END_PHASE; /* A start tag with the tag name "noframes" */ } elseif ($token['name'] === 'noframes' && $token['type'] === HTML5::STARTTAG ) { /* Process the token as if the insertion mode had been "in body". */ $this->inBody($token); /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function trailingEndPhase($token) { /* After the main phase, as each token is emitted from the tokenisation stage, it must be processed as described in this section. */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the Document object with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->dom->appendChild($comment); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Process the token as it would be processed in the main phase. */ $this->mainPhase($token); /* A character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. Or a start tag token. Or an end tag token. */ } elseif (($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG ) { /* Parse error. Switch back to the main phase and reprocess the token. */ $this->phase = self::MAIN_PHASE; return $this->mainPhase($token); /* An end-of-file token */ } elseif ($token['type'] === HTML5::EOF) { /* OMG DONE!! */ } } private function insertElement($token, $append = true, $check = false) { // Proprietary workaround for libxml2's limitations with tag names if ($check) { // Slightly modified HTML5 tag-name modification, // removing anything that's not an ASCII letter, digit, or hyphen $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); // Remove leading hyphens and numbers $token['name'] = ltrim($token['name'], '-0..9'); // In theory, this should ever be needed, but just in case if ($token['name'] === '') { $token['name'] = 'span'; } // arbitrary generic choice } $el = $this->dom->createElement($token['name']); foreach ($token['attr'] as $attr) { if (!$el->hasAttribute($attr['name'])) { $el->setAttribute($attr['name'], (string)$attr['value']); } } $this->appendToRealParent($el); $this->stack[] = $el; return $el; } private function insertText($data) { $text = $this->dom->createTextNode($data); $this->appendToRealParent($text); } private function insertComment($data) { $comment = $this->dom->createComment($data); $this->appendToRealParent($comment); } private function appendToRealParent($node) { if ($this->foster_parent === null) { end($this->stack)->appendChild($node); } elseif ($this->foster_parent !== null) { /* If the foster parent element is the parent element of the last table element in the stack of open elements, then the new node must be inserted immediately before the last table element in the stack of open elements in the foster parent element; otherwise, the new node must be appended to the foster parent element. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === 'table' && $this->stack[$n]->parentNode !== null ) { $table = $this->stack[$n]; break; } } if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { $this->foster_parent->insertBefore($node, $table); } else { $this->foster_parent->appendChild($node); } $this->foster_parent = null; } } private function elementInScope($el, $table = false) { if (is_array($el)) { foreach ($el as $element) { if ($this->elementInScope($element, $table)) { return true; } } return false; } $leng = count($this->stack); for ($n = 0; $n < $leng; $n++) { /* 1. Initialise node to be the current node (the bottommost node of the stack). */ $node = $this->stack[$leng - 1 - $n]; if ($node->tagName === $el) { /* 2. If node is the target node, terminate in a match state. */ return true; } elseif ($node->tagName === 'table') { /* 3. Otherwise, if node is a table element, terminate in a failure state. */ return false; } elseif ($table === true && in_array( $node->tagName, array( 'caption', 'td', 'th', 'button', 'marquee', 'object' ) ) ) { /* 4. Otherwise, if the algorithm is the "has an element in scope" variant (rather than the "has an element in table scope" variant), and node is one of the following, terminate in a failure state. */ return false; } elseif ($node === $node->ownerDocument->documentElement) { /* 5. Otherwise, if node is an html element (root element), terminate in a failure state. (This can only happen if the node is the topmost node of the stack of open elements, and prevents the next step from being invoked if there are no more elements in the stack.) */ return false; } /* Otherwise, set node to the previous entry in the stack of open elements and return to step 2. (This will never fail, since the loop will always terminate in the previous step if the top of the stack is reached.) */ } } private function reconstructActiveFormattingElements() { /* 1. If there are no entries in the list of active formatting elements, then there is nothing to reconstruct; stop this algorithm. */ $formatting_elements = count($this->a_formatting); if ($formatting_elements === 0) { return false; } /* 3. Let entry be the last (most recently added) element in the list of active formatting elements. */ $entry = end($this->a_formatting); /* 2. If the last (most recently added) entry in the list of active formatting elements is a marker, or if it is an element that is in the stack of open elements, then there is nothing to reconstruct; stop this algorithm. */ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { return false; } for ($a = $formatting_elements - 1; $a >= 0; true) { /* 4. If there are no entries before entry in the list of active formatting elements, then jump to step 8. */ if ($a === 0) { $step_seven = false; break; } /* 5. Let entry be the entry one earlier than entry in the list of active formatting elements. */ $a--; $entry = $this->a_formatting[$a]; /* 6. If entry is neither a marker nor an element that is also in thetack of open elements, go to step 4. */ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { break; } } while (true) { /* 7. Let entry be the element one later than entry in the list of active formatting elements. */ if (isset($step_seven) && $step_seven === true) { $a++; $entry = $this->a_formatting[$a]; } /* 8. Perform a shallow clone of the element entry to obtain clone. */ $clone = $entry->cloneNode(); /* 9. Append clone to the current node and push it onto the stack of open elements so that it is the new current node. */ end($this->stack)->appendChild($clone); $this->stack[] = $clone; /* 10. Replace the entry for entry in the list with an entry for clone. */ $this->a_formatting[$a] = $clone; /* 11. If the entry for clone in the list of active formatting elements is not the last entry in the list, return to step 7. */ if (end($this->a_formatting) !== $clone) { $step_seven = true; } else { break; } } } private function clearTheActiveFormattingElementsUpToTheLastMarker() { /* When the steps below require the UA to clear the list of active formatting elements up to the last marker, the UA must perform the following steps: */ while (true) { /* 1. Let entry be the last (most recently added) entry in the list of active formatting elements. */ $entry = end($this->a_formatting); /* 2. Remove entry from the list of active formatting elements. */ array_pop($this->a_formatting); /* 3. If entry was a marker, then stop the algorithm at this point. The list has been cleared up to the last marker. */ if ($entry === self::MARKER) { break; } } } private function generateImpliedEndTags($exclude = array()) { /* When the steps below require the UA to generate implied end tags, then, if the current node is a dd element, a dt element, an li element, a p element, a td element, a th element, or a tr element, the UA must act as if an end tag with the respective tag name had been seen and then generate implied end tags again. */ $node = end($this->stack); $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); while (in_array(end($this->stack)->nodeName, $elements)) { array_pop($this->stack); } } private function getElementCategory($node) { $name = $node->tagName; if (in_array($name, $this->special)) { return self::SPECIAL; } elseif (in_array($name, $this->scoping)) { return self::SCOPING; } elseif (in_array($name, $this->formatting)) { return self::FORMATTING; } else { return self::PHRASING; } } private function clearStackToTableContext($elements) { /* When the steps above require the UA to clear the stack back to a table context, it means that the UA must, while the current node is not a table element or an html element, pop elements from the stack of open elements. If this causes any elements to be popped from the stack, then this is a parse error. */ while (true) { $node = end($this->stack)->nodeName; if (in_array($node, $elements)) { break; } else { array_pop($this->stack); } } } private function resetInsertionMode() { /* 1. Let last be false. */ $last = false; $leng = count($this->stack); for ($n = $leng - 1; $n >= 0; $n--) { /* 2. Let node be the last node in the stack of open elements. */ $node = $this->stack[$n]; /* 3. If node is the first node in the stack of open elements, then set last to true. If the element whose innerHTML attribute is being set is neither a td element nor a th element, then set node to the element whose innerHTML attribute is being set. (innerHTML case) */ if ($this->stack[0]->isSameNode($node)) { $last = true; } /* 4. If node is a select element, then switch the insertion mode to "in select" and abort these steps. (innerHTML case) */ if ($node->nodeName === 'select') { $this->mode = self::IN_SELECT; break; /* 5. If node is a td or th element, then switch the insertion mode to "in cell" and abort these steps. */ } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { $this->mode = self::IN_CELL; break; /* 6. If node is a tr element, then switch the insertion mode to "in row" and abort these steps. */ } elseif ($node->nodeName === 'tr') { $this->mode = self::IN_ROW; break; /* 7. If node is a tbody, thead, or tfoot element, then switch the insertion mode to "in table body" and abort these steps. */ } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { $this->mode = self::IN_TBODY; break; /* 8. If node is a caption element, then switch the insertion mode to "in caption" and abort these steps. */ } elseif ($node->nodeName === 'caption') { $this->mode = self::IN_CAPTION; break; /* 9. If node is a colgroup element, then switch the insertion mode to "in column group" and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'colgroup') { $this->mode = self::IN_CGROUP; break; /* 10. If node is a table element, then switch the insertion mode to "in table" and abort these steps. */ } elseif ($node->nodeName === 'table') { $this->mode = self::IN_TABLE; break; /* 11. If node is a head element, then switch the insertion mode to "in body" ("in body"! not "in head"!) and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'head') { $this->mode = self::IN_BODY; break; /* 12. If node is a body element, then switch the insertion mode to "in body" and abort these steps. */ } elseif ($node->nodeName === 'body') { $this->mode = self::IN_BODY; break; /* 13. If node is a frameset element, then switch the insertion mode to "in frameset" and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'frameset') { $this->mode = self::IN_FRAME; break; /* 14. If node is an html element, then: if the head element pointer is null, switch the insertion mode to "before head", otherwise, switch the insertion mode to "after head". In either case, abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'html') { $this->mode = ($this->head_pointer === null) ? self::BEFOR_HEAD : self::AFTER_HEAD; break; /* 15. If last is true, then set the insertion mode to "in body" and abort these steps. (innerHTML case) */ } elseif ($last) { $this->mode = self::IN_BODY; break; } } } private function closeCell() { /* If the stack of open elements has a td or th element in table scope, then act as if an end tag token with that tag name had been seen. */ foreach (array('td', 'th') as $cell) { if ($this->elementInScope($cell, true)) { $this->inCell( array( 'name' => $cell, 'type' => HTML5::ENDTAG ) ); break; } } } public function save() { return $this->dom; } } HTMLPurifier/Language.php 0000644 00000013656 15111210775 0011265 0 ustar 00 <?php /** * Represents a language and defines localizable string formatting and * other functions, as well as the localized messages for HTML Purifier. */ class HTMLPurifier_Language { /** * ISO 639 language code of language. Prefers shortest possible version. * @type string */ public $code = 'en'; /** * Fallback language code. * @type bool|string */ public $fallback = false; /** * Array of localizable messages. * @type array */ public $messages = array(); /** * Array of localizable error codes. * @type array */ public $errorNames = array(); /** * True if no message file was found for this language, so English * is being used instead. Check this if you'd like to notify the * user that they've used a non-supported language. * @type bool */ public $error = false; /** * Has the language object been loaded yet? * @type bool * @todo Make it private, fix usage in HTMLPurifier_LanguageTest */ public $_loaded = false; /** * @type HTMLPurifier_Config */ protected $config; /** * @type HTMLPurifier_Context */ protected $context; /** * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context */ public function __construct($config, $context) { $this->config = $config; $this->context = $context; } /** * Loads language object with necessary info from factory cache * @note This is a lazy loader */ public function load() { if ($this->_loaded) { return; } $factory = HTMLPurifier_LanguageFactory::instance(); $factory->loadLanguage($this->code); foreach ($factory->keys as $key) { $this->$key = $factory->cache[$this->code][$key]; } $this->_loaded = true; } /** * Retrieves a localised message. * @param string $key string identifier of message * @return string localised message */ public function getMessage($key) { if (!$this->_loaded) { $this->load(); } if (!isset($this->messages[$key])) { return "[$key]"; } return $this->messages[$key]; } /** * Retrieves a localised error name. * @param int $int error number, corresponding to PHP's error reporting * @return string localised message */ public function getErrorName($int) { if (!$this->_loaded) { $this->load(); } if (!isset($this->errorNames[$int])) { return "[Error: $int]"; } return $this->errorNames[$int]; } /** * Converts an array list into a string readable representation * @param array $array * @return string */ public function listify($array) { $sep = $this->getMessage('Item separator'); $sep_last = $this->getMessage('Item separator last'); $ret = ''; for ($i = 0, $c = count($array); $i < $c; $i++) { if ($i == 0) { } elseif ($i + 1 < $c) { $ret .= $sep; } else { $ret .= $sep_last; } $ret .= $array[$i]; } return $ret; } /** * Formats a localised message with passed parameters * @param string $key string identifier of message * @param array $args Parameters to substitute in * @return string localised message * @todo Implement conditionals? Right now, some messages make * reference to line numbers, but those aren't always available */ public function formatMessage($key, $args = array()) { if (!$this->_loaded) { $this->load(); } if (!isset($this->messages[$key])) { return "[$key]"; } $raw = $this->messages[$key]; $subst = array(); $generator = false; foreach ($args as $i => $value) { if (is_object($value)) { if ($value instanceof HTMLPurifier_Token) { // factor this out some time if (!$generator) { $generator = $this->context->get('Generator'); } if (isset($value->name)) { $subst['$'.$i.'.Name'] = $value->name; } if (isset($value->data)) { $subst['$'.$i.'.Data'] = $value->data; } $subst['$'.$i.'.Compact'] = $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value); // a more complex algorithm for compact representation // could be introduced for all types of tokens. This // may need to be factored out into a dedicated class if (!empty($value->attr)) { $stripped_token = clone $value; $stripped_token->attr = array(); $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token); } $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown'; } continue; } elseif (is_array($value)) { $keys = array_keys($value); if (array_keys($keys) === $keys) { // list $subst['$'.$i] = $this->listify($value); } else { // associative array // no $i implementation yet, sorry $subst['$'.$i.'.Keys'] = $this->listify($keys); $subst['$'.$i.'.Values'] = $this->listify(array_values($value)); } continue; } $subst['$' . $i] = $value; } return strtr($raw, $subst); } } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema.php 0000644 00000013417 15111210775 0012063 0 ustar 00 <?php /** * Configuration definition, defines directives and their defaults. */ class HTMLPurifier_ConfigSchema { /** * Defaults of the directives and namespaces. * @type array * @note This shares the exact same structure as HTMLPurifier_Config::$conf */ public $defaults = array(); /** * The default property list. Do not edit this property list. * @type array */ public $defaultPlist; /** * Definition of the directives. * The structure of this is: * * array( * 'Namespace' => array( * 'Directive' => new stdClass(), * ) * ) * * The stdClass may have the following properties: * * - If isAlias isn't set: * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions * - allow_null: If set, this directive allows null values * - aliases: If set, an associative array of value aliases to real values * - allowed: If set, a lookup array of allowed (string) values * - If isAlias is set: * - namespace: Namespace this directive aliases to * - name: Directive name this directive aliases to * * In certain degenerate cases, stdClass will actually be an integer. In * that case, the value is equivalent to an stdClass with the type * property set to the integer. If the integer is negative, type is * equal to the absolute value of integer, and allow_null is true. * * This class is friendly with HTMLPurifier_Config. If you need introspection * about the schema, you're better of using the ConfigSchema_Interchange, * which uses more memory but has much richer information. * @type array */ public $info = array(); /** * Application-wide singleton * @type HTMLPurifier_ConfigSchema */ protected static $singleton; public function __construct() { $this->defaultPlist = new HTMLPurifier_PropertyList(); } /** * Unserializes the default ConfigSchema. * @return HTMLPurifier_ConfigSchema */ public static function makeFromSerial() { $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); $r = unserialize($contents); if (!$r) { $hash = sha1($contents); trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); } return $r; } /** * Retrieves an instance of the application-wide configuration definition. * @param HTMLPurifier_ConfigSchema $prototype * @return HTMLPurifier_ConfigSchema */ public static function instance($prototype = null) { if ($prototype !== null) { HTMLPurifier_ConfigSchema::$singleton = $prototype; } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); } return HTMLPurifier_ConfigSchema::$singleton; } /** * Defines a directive for configuration * @warning Will fail of directive's namespace is defined. * @warning This method's signature is slightly different from the legacy * define() static method! Beware! * @param string $key Name of directive * @param mixed $default Default value of directive * @param string $type Allowed type of the directive. See * HTMLPurifier_VarParser::$types for allowed values * @param bool $allow_null Whether or not to allow null values */ public function add($key, $default, $type, $allow_null) { $obj = new stdClass(); $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; if ($allow_null) { $obj->allow_null = true; } $this->info[$key] = $obj; $this->defaults[$key] = $default; $this->defaultPlist->set($key, $default); } /** * Defines a directive value alias. * * Directive value aliases are convenient for developers because it lets * them set a directive to several values and get the same result. * @param string $key Name of Directive * @param array $aliases Hash of aliased values to the real alias */ public function addValueAliases($key, $aliases) { if (!isset($this->info[$key]->aliases)) { $this->info[$key]->aliases = array(); } foreach ($aliases as $alias => $real) { $this->info[$key]->aliases[$alias] = $real; } } /** * Defines a set of allowed values for a directive. * @warning This is slightly different from the corresponding static * method definition. * @param string $key Name of directive * @param array $allowed Lookup array of allowed values */ public function addAllowedValues($key, $allowed) { $this->info[$key]->allowed = $allowed; } /** * Defines a directive alias for backwards compatibility * @param string $key Directive that will be aliased * @param string $new_key Directive that the alias will be to */ public function addAlias($key, $new_key) { $obj = new stdClass; $obj->key = $new_key; $obj->isAlias = true; $this->info[$key] = $obj; } /** * Replaces any stdClass that only has the type property with type integer. */ public function postProcess() { foreach ($this->info as $key => $v) { if (count((array) $v) == 1) { $this->info[$key] = $v->type; } elseif (count((array) $v) == 2 && isset($v->allow_null)) { $this->info[$key] = -$v->type; } } } } // vim: et sw=4 sts=4 HTMLPurifier/PercentEncoder.php 0000644 00000006755 15111210775 0012444 0 ustar 00 <?php /** * Class that handles operations involving percent-encoding in URIs. * * @warning * Be careful when reusing instances of PercentEncoder. The object * you use for normalize() SHOULD NOT be used for encode(), or * vice-versa. */ class HTMLPurifier_PercentEncoder { /** * Reserved characters to preserve when using encode(). * @type array */ protected $preserve = array(); /** * String of characters that should be preserved while using encode(). * @param bool $preserve */ public function __construct($preserve = false) { // unreserved letters, ought to const-ify for ($i = 48; $i <= 57; $i++) { // digits $this->preserve[$i] = true; } for ($i = 65; $i <= 90; $i++) { // upper-case $this->preserve[$i] = true; } for ($i = 97; $i <= 122; $i++) { // lower-case $this->preserve[$i] = true; } $this->preserve[45] = true; // Dash - $this->preserve[46] = true; // Period . $this->preserve[95] = true; // Underscore _ $this->preserve[126]= true; // Tilde ~ // extra letters not to escape if ($preserve !== false) { for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { $this->preserve[ord($preserve[$i])] = true; } } } /** * Our replacement for urlencode, it encodes all non-reserved characters, * as well as any extra characters that were instructed to be preserved. * @note * Assumes that the string has already been normalized, making any * and all percent escape sequences valid. Percents will not be * re-escaped, regardless of their status in $preserve * @param string $string String to be encoded * @return string Encoded string. */ public function encode($string) { $ret = ''; for ($i = 0, $c = strlen($string); $i < $c; $i++) { if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { $ret .= '%' . sprintf('%02X', $int); } else { $ret .= $string[$i]; } } return $ret; } /** * Fix up percent-encoding by decoding unreserved characters and normalizing. * @warning This function is affected by $preserve, even though the * usual desired behavior is for this not to preserve those * characters. Be careful when reusing instances of PercentEncoder! * @param string $string String to normalize * @return string */ public function normalize($string) { if ($string == '') { return ''; } $parts = explode('%', $string); $ret = array_shift($parts); foreach ($parts as $part) { $length = strlen($part); if ($length < 2) { $ret .= '%25' . $part; continue; } $encoding = substr($part, 0, 2); $text = substr($part, 2); if (!ctype_xdigit($encoding)) { $ret .= '%25' . $part; continue; } $int = hexdec($encoding); if (isset($this->preserve[$int])) { $ret .= chr($int) . $text; continue; } $encoding = strtoupper($encoding); $ret .= '%' . $encoding . $text; } return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/AttrDef.php 0000644 00000012113 15111210775 0011056 0 ustar 00 <?php /** * Base class for all validating attribute definitions. * * This family of classes forms the core for not only HTML attribute validation, * but also any sort of string that needs to be validated or cleaned (which * means CSS properties and composite definitions are defined here too). * Besides defining (through code) what precisely makes the string valid, * subclasses are also responsible for cleaning the code if possible. */ abstract class HTMLPurifier_AttrDef { /** * Tells us whether or not an HTML attribute is minimized. * Has no meaning in other contexts. * @type bool */ public $minimized = false; /** * Tells us whether or not an HTML attribute is required. * Has no meaning in other contexts * @type bool */ public $required = false; /** * Validates and cleans passed string according to a definition. * * @param string $string String to be validated and cleaned. * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object. */ abstract public function validate($string, $config, $context); /** * Convenience method that parses a string as if it were CDATA. * * This method process a string in the manner specified at * <http://www.w3.org/TR/html4/types.html#h-6.2> by removing * leading and trailing whitespace, ignoring line feeds, and replacing * carriage returns and tabs with spaces. While most useful for HTML * attributes specified as CDATA, it can also be applied to most CSS * values. * * @note This method is not entirely standards compliant, as trim() removes * more types of whitespace than specified in the spec. In practice, * this is rarely a problem, as those extra characters usually have * already been removed by HTMLPurifier_Encoder. * * @warning This processing is inconsistent with XML's whitespace handling * as specified by section 3.3.3 and referenced XHTML 1.0 section * 4.7. However, note that we are NOT necessarily * parsing XML, thus, this behavior may still be correct. We * assume that newlines have been normalized. */ public function parseCDATA($string) { $string = trim($string); $string = str_replace(array("\n", "\t", "\r"), ' ', $string); return $string; } /** * Factory method for creating this class from a string. * @param string $string String construction info * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string */ public function make($string) { // default implementation, return a flyweight of this object. // If $string has an effect on the returned object (i.e. you // need to overload this method), it is best // to clone or instantiate new copies. (Instantiation is safer.) return $this; } /** * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work * properly. THIS IS A HACK! * @param string $string a CSS colour definition * @return string */ protected function mungeRgb($string) { $p = '\s*(\d+(\.\d+)?([%]?))\s*'; if (preg_match('/(rgba|hsla)\(/', $string)) { return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); } return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); } /** * Parses a possibly escaped CSS string and returns the "pure" * version of it. */ protected function expandCSSEscape($string) { // flexibly parse it $ret = ''; for ($i = 0, $c = strlen($string); $i < $c; $i++) { if ($string[$i] === '\\') { $i++; if ($i >= $c) { $ret .= '\\'; break; } if (ctype_xdigit($string[$i])) { $code = $string[$i]; for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { if (!ctype_xdigit($string[$i])) { break; } $code .= $string[$i]; } // We have to be extremely careful when adding // new characters, to make sure we're not breaking // the encoding. $char = HTMLPurifier_Encoder::unichr(hexdec($code)); if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { continue; } $ret .= $char; if ($i < $c && trim($string[$i]) !== '') { $i--; } continue; } if ($string[$i] === "\n") { continue; } } $ret .= $string[$i]; } return $ret; } } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/Interchange.php 0000644 00000002402 15111210775 0014302 0 ustar 00 <?php /** * Generic schema interchange format that can be converted to a runtime * representation (HTMLPurifier_ConfigSchema) or HTML documentation. Members * are completely validated. */ class HTMLPurifier_ConfigSchema_Interchange { /** * Name of the application this schema is describing. * @type string */ public $name; /** * Array of Directive ID => array(directive info) * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] */ public $directives = array(); /** * Adds a directive array to $directives * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive * @throws HTMLPurifier_ConfigSchema_Exception */ public function addDirective($directive) { if (isset($this->directives[$i = $directive->id->toString()])) { throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); } $this->directives[$i] = $directive; } /** * Convenience function to perform standard validation. Throws exception * on failed validation. */ public function validate() { $validator = new HTMLPurifier_ConfigSchema_Validator(); return $validator->validate($this); } } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt 0000644 00000000501 15111210775 0017745 0 ustar 00 Core.AllowParseManyTags TYPE: bool DEFAULT: false VERSION: 4.10.1 --DESCRIPTION-- <p> This directive allows parsing of many nested tags. If you set true, relaxes any hardcoded limit from the parser. However, in that case it may cause a Dos attack. Be careful when enabling it. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt 0000644 00000000516 15111210775 0017437 0 ustar 00 Core.RemoveInvalidImg TYPE: bool DEFAULT: true VERSION: 1.3.0 --DESCRIPTION-- <p> This directive enables pre-emptive URI checking in <code>img</code> tags, as the attribute validation strategy is not authorized to remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt 0000644 00000000500 15111210775 0020107 0 ustar 00 Attr.DefaultInvalidImage TYPE: string DEFAULT: '' --DESCRIPTION-- This is the default image an img tag will be pointed to if it does not have a valid src attribute. In future versions, we may allow the image tag to be removed completely, but due to design issues, this is not possible right now. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt 0000644 00000001170 15111210775 0021577 0 ustar 00 AutoFormat.RemoveEmpty.Predicate TYPE: hash VERSION: 4.7.0 DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src')) --DESCRIPTION-- <p> Given that an element has no contents, it will be removed by default, unless this predicate dictates otherwise. The predicate can either be an associative map from tag name to list of attributes that must be present for the element to be considered preserved: thus, the default always preserves <code>colgroup</code>, <code>th</code> and <code>td</code>, and also <code>iframe</code> if it has a <code>src</code>. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt 0000644 00000001067 15111210775 0020664 0 ustar 00 Core.EscapeNonASCIICharacters TYPE: bool VERSION: 1.4.0 DEFAULT: false --DESCRIPTION-- This directive overcomes a deficiency in %Core.Encoding by blindly converting all non-ASCII characters into decimal numeric entities before converting it to its native encoding. This means that even characters that can be expressed in the non-UTF-8 encoding will be entity-ized, which can be a real downer for encodings like Big5. It also assumes that the ASCII repetoire is available, although this is the case for almost all encodings. Anyway, use UTF-8! --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt 0000644 00000000757 15111210775 0016176 0 ustar 00 Filter.YouTube TYPE: bool VERSION: 3.1.0 DEFAULT: false --DESCRIPTION-- <p> <strong>Warning:</strong> Deprecated in favor of %HTML.SafeObject and %Output.FlashCompat (turn both on to allow YouTube videos and other Flash content). </p> <p> This directive enables YouTube video embedding in HTML Purifier. Check <a href="http://htmlpurifier.org/docs/enduser-youtube.html">this document on embedding videos</a> for more information on what this filter does. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.Newline.txt 0000644 00000000546 15111210775 0016252 0 ustar 00 Output.Newline TYPE: string/null VERSION: 2.0.1 DEFAULT: NULL --DESCRIPTION-- <p> Newline string to format final output with. If left null, HTML Purifier will auto-detect the default newline type of the system and use that; you can manually override it here. Remember, \r\n is Windows, \r is Mac, and \n is Unix. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.Host.txt 0000644 00000001462 15111210775 0014723 0 ustar 00 URI.Host TYPE: string/null VERSION: 1.2.0 DEFAULT: NULL --DESCRIPTION-- <p> Defines the domain name of the server, so we can determine whether or an absolute URI is from your website or not. Not strictly necessary, as users should be using relative URIs to reference resources on your website. It will, however, let you use absolute URIs to link to subdomains of the domain you post here: i.e. example.com will allow sub.example.com. However, higher up domains will still be excluded: if you set %URI.Host to sub.example.com, example.com will be blocked. <strong>Note:</strong> This directive overrides %URI.Base because a given page may be on a sub-domain, but you wish HTML Purifier to be more relaxed and allow some of the parent domains too. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt 0000644 00000000413 15111210775 0017030 0 ustar 00 AutoFormat.Linkify TYPE: bool VERSION: 2.0.1 DEFAULT: false --DESCRIPTION-- <p> This directive turns on linkification, auto-linking http, ftp and https URLs. <code>a</code> tags with the <code>href</code> attribute must be allowed. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt 0000644 00000000537 15111210775 0016402 0 ustar 00 CSS.AllowedFonts TYPE: lookup/null VERSION: 4.3.0 DEFAULT: NULL --DESCRIPTION-- <p> Allows you to manually specify a set of allowed fonts. If <code>NULL</code>, all fonts are allowed. This directive affects generic names (serif, sans-serif, monospace, cursive, fantasy) as well as specific font families. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt 0000644 00000000566 15111210775 0017270 0 ustar 00 Cache.DefinitionImpl TYPE: string/null VERSION: 2.0.0 DEFAULT: 'Serializer' --DESCRIPTION-- This directive defines which method to use when caching definitions, the complex data-type that makes HTML Purifier tick. Set to null to disable caching (not recommended, as you will see a definite performance degradation). --ALIASES-- Core.DefinitionCache --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt 0000644 00000000733 15111210775 0015733 0 ustar 00 Attr.IDPrefix TYPE: string VERSION: 1.2.0 DEFAULT: '' --DESCRIPTION-- String to prefix to IDs. If you have no idea what IDs your pages may use, you may opt to simply add a prefix to all user-submitted ID attributes so that they are still usable, but will not conflict with core page IDs. Example: setting the directive to 'user_' will result in a user submitted 'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true before using this. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt 0000644 00000000475 15111210775 0017564 0 ustar 00 Attr.IDBlacklistRegexp TYPE: string/null VERSION: 1.6.0 DEFAULT: NULL --DESCRIPTION-- PCRE regular expression to be matched against all IDs. If the expression is matches, the ID is rejected. Use this with care: may cause significant degradation. ID matching is done after all other validation. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt 0000644 00000000440 15111210775 0016306 0 ustar 00 Attr.AllowedRel TYPE: lookup VERSION: 1.6.0 DEFAULT: array() --DESCRIPTION-- List of allowed forward document relationships in the rel attribute. Common values may be nofollow or print. By default, this is empty, meaning that no document relationships are allowed. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt 0000644 00000000243 15111210775 0015706 0 ustar 00 HTML.Nofollow TYPE: bool VERSION: 4.3.0 DEFAULT: FALSE --DESCRIPTION-- If enabled, nofollow rel attributes are added to all outgoing links. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt 0000644 00000001137 15111210776 0017551 0 ustar 00 HTML.AllowedAttributes TYPE: lookup/null VERSION: 1.3.0 DEFAULT: NULL --DESCRIPTION-- <p> If HTML Purifier's attribute set is unsatisfactory, overload it! The syntax is "tag.attr" or "*.attr" for the global attributes (style, id, class, dir, lang, xml:lang). </p> <p> <strong>Warning:</strong> If another directive conflicts with the elements here, <em>that</em> directive will win and override. For example, %HTML.EnableAttrID will take precedence over *.id in this directive. You must set that directive to true before you can use IDs at all. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt 0000644 00000002024 15111210776 0016134 0 ustar 00 Core.LexerImpl TYPE: mixed/null VERSION: 2.0.0 DEFAULT: NULL --DESCRIPTION-- <p> This parameter determines what lexer implementation can be used. The valid values are: </p> <dl> <dt><em>null</em></dt> <dd> Recommended, the lexer implementation will be auto-detected based on your PHP-version and configuration. </dd> <dt><em>string</em> lexer identifier</dt> <dd> This is a slim way of manually overridding the implementation. Currently recognized values are: DOMLex (the default PHP5 implementation) and DirectLex (the default PHP4 implementation). Only use this if you know what you are doing: usually, the auto-detection will manage things for cases you aren't even aware of. </dd> <dt><em>object</em> lexer instance</dt> <dd> Super-advanced: you can specify your own, custom, implementation that implements the interface defined by <code>HTMLPurifier_Lexer</code>. I may remove this option simply because I don't expect anyone to use it. </dd> </dl> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt 0000644 00000000606 15111210776 0016362 0 ustar 00 URI.MakeAbsolute TYPE: bool VERSION: 2.1.0 DEFAULT: false --DESCRIPTION-- <p> Converts all URIs into absolute forms. This is useful when the HTML being filtered assumes a specific base path, but will actually be viewed in a different context (and setting an alternate base URI is not possible). %URI.Base must be set for this directive to work. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt 0000644 00000000354 15111210776 0016753 0 ustar 00 CSS.AllowImportant TYPE: bool DEFAULT: false VERSION: 3.1.0 --DESCRIPTION-- This parameter determines whether or not !important cascade modifiers should be allowed in user CSS. If false, !important will stripped. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt 0000644 00000000423 15111210776 0021151 0 ustar 00 Output.CommentScriptContents TYPE: bool VERSION: 2.0.0 DEFAULT: true --DESCRIPTION-- Determines whether or not HTML Purifier should attempt to fix up the contents of script tags for legacy browsers with comments. --ALIASES-- Core.CommentScriptContents --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt 0000644 00000000461 15111210776 0017046 0 ustar 00 --# vim: et sw=4 sts=4 HTML.TargetNoopener TYPE: bool VERSION: 4.8.0 DEFAULT: TRUE --DESCRIPTION-- If enabled, noopener rel attributes are added to links which have a target attribute associated with them. This prevents malicious destinations from overwriting the original window. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt 0000644 00000001377 15111210776 0015773 0 ustar 00 Core.Encoding TYPE: istring DEFAULT: 'utf-8' --DESCRIPTION-- If for some reason you are unable to convert all webpages to UTF-8, you can use this directive as a stop-gap compatibility change to let HTML Purifier deal with non UTF-8 input. This technique has notable deficiencies: absolutely no characters outside of the selected character encoding will be preserved, not even the ones that have been ampersand escaped (this is due to a UTF-8 specific <em>feature</em> that automatically resolves all entities), making it pretty useless for anything except the most I18N-blind applications, although %Core.EscapeNonASCIICharacters offers fixes this trouble with another tradeoff. This directive only accepts ISO-8859-1 if iconv is not enabled. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt 0000644 00000000717 15111210776 0017753 0 ustar 00 CSS.ForbiddenProperties TYPE: lookup VERSION: 4.2.0 DEFAULT: array() --DESCRIPTION-- <p> This is the logical inverse of %CSS.AllowedProperties, and it will override that directive or any other directive. If possible, %CSS.AllowedProperties is recommended over this directive, because it can sometimes be difficult to tell whether or not you've forbidden all of the CSS properties you truly would like to disallow. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt 0000644 00000000515 15111210777 0015201 0 ustar 00 HTML.Forms TYPE: bool VERSION: 4.13.0 DEFAULT: false --DESCRIPTION-- <p> Whether or not to permit form elements in the user input, regardless of %HTML.Trusted value. Please be very careful when using this functionality, as enabling forms in untrusted documents may allow for phishing attacks. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt 0000644 00000001140 15111210777 0017436 0 ustar 00 CSS.AllowedProperties TYPE: lookup/null VERSION: 3.1.0 DEFAULT: NULL --DESCRIPTION-- <p> If HTML Purifier's style attributes set is unsatisfactory for your needs, you can overload it with your own list of tags to allow. Note that this method is subtractive: it does its job by taking away from HTML Purifier usual feature set, so you cannot add an attribute that HTML Purifier never supported in the first place. </p> <p> <strong>Warning:</strong> If another directive conflicts with the elements here, <em>that</em> directive will win and override. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt 0000644 00000002260 15111210777 0021434 0 ustar 00 Filter.ExtractStyleBlocks.Scope TYPE: string/null VERSION: 3.0.0 DEFAULT: NULL ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope --DESCRIPTION-- <p> If you would like users to be able to define external stylesheets, but only allow them to specify CSS declarations for a specific node and prevent them from fiddling with other elements, use this directive. It accepts any valid CSS selector, and will prepend this to any CSS declaration extracted from the document. For example, if this directive is set to <code>#user-content</code> and a user uses the selector <code>a:hover</code>, the final selector will be <code>#user-content a:hover</code>. </p> <p> The comma shorthand may be used; consider the above example, with <code>#user-content, #user-content2</code>, the final selector will be <code>#user-content a:hover, #user-content2 a:hover</code>. </p> <p> <strong>Warning:</strong> It is possible for users to bypass this measure using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML Purifier, and I am working to get it fixed. Until then, HTML Purifier performs a basic check to prevent this. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt 0000644 00000000466 15111210777 0016561 0 ustar 00 URI.HostBlacklist TYPE: list VERSION: 1.3.0 DEFAULT: array() --DESCRIPTION-- List of strings that are forbidden in the host of any URI. Use it to kill domain names of spam, etc. Note that it will catch anything in the domain, so <tt>moo.com</tt> will catch <tt>moo.com.example.com</tt>. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt 0000644 00000001100 15111210777 0022106 0 ustar 00 Filter.ExtractStyleBlocks.TidyImpl TYPE: mixed/null VERSION: 3.1.0 DEFAULT: NULL ALIASES: FilterParam.ExtractStyleBlocksTidyImpl --DESCRIPTION-- <p> If left NULL, HTML Purifier will attempt to instantiate a <code>csstidy</code> class to use for internal cleaning. This will usually be good enough. </p> <p> However, for trusted user input, you can set this to <code>false</code> to disable cleaning. In addition, you can supply your own concrete implementation of Tidy's interface to use, although I don't know why you'd want to do that. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt 0000644 00000001170 15111211000 0022417 0 ustar 00 Core.DirectLexLineNumberSyncInterval TYPE: int VERSION: 2.0.0 DEFAULT: 0 --DESCRIPTION-- <p> Specifies the number of tokens the DirectLex line number tracking implementations should process before attempting to resyncronize the current line count by manually counting all previous new-lines. When at 0, this functionality is disabled. Lower values will decrease performance, and this is only strictly necessary if the counting algorithm is buggy (in which case you should report it as a bug). This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is not being used. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt 0000644 00000000464 15111211000 0020522 0 ustar 00 AutoFormat.PurifierLinkify TYPE: bool VERSION: 2.0.1 DEFAULT: false --DESCRIPTION-- <p> Internal auto-formatter that converts configuration directives in syntax <a>%Namespace.Directive</a> to links. <code>a</code> tags with the <code>href</code> attribute must be allowed. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt 0000644 00000000304 15111211000 0016523 0 ustar 00 URI.DefinitionRev TYPE: int VERSION: 2.1.0 DEFAULT: 1 --DESCRIPTION-- <p> Revision identifier for your custom definition. See %HTML.DefinitionRev for details. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt 0000644 00000000430 15111211000 0020347 0 ustar 00 Core.RemoveScriptContents TYPE: bool/null DEFAULT: NULL VERSION: 2.0.0 DEPRECATED-VERSION: 2.1.0 DEPRECATED-USE: Core.HiddenElements --DESCRIPTION-- <p> This directive enables HTML Purifier to remove not only script tags but all of their contents. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt 0000644 00000001077 15111211000 0021026 0 ustar 00 Core.AllowHostnameUnderscore TYPE: bool VERSION: 4.6.0 DEFAULT: false --DESCRIPTION-- <p> By RFC 1123, underscores are not permitted in host names. (This is in contrast to the specification for DNS, RFC 2181, which allows underscores.) However, most browsers do the right thing when faced with an underscore in the host name, and so some poorly written websites are written with the expectation this should work. Setting this parameter to true relaxes our allowed character check so that underscores are permitted. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt 0000644 00000000406 15111211000 0017446 0 ustar 00 Attr.ForbiddenClasses TYPE: lookup VERSION: 4.0.0 DEFAULT: array() --DESCRIPTION-- List of forbidden class values in the class attribute. By default, this is empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt 0000644 00000000375 15111211000 0016271 0 ustar 00 HTML.TargetBlank TYPE: bool VERSION: 4.4.0 DEFAULT: FALSE --DESCRIPTION-- If enabled, <code>target=blank</code> attributes are added to all outgoing links. (This includes links from an HTTPS version of a page to an HTTP version.) --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt 0000644 00000001744 15111211001 0015464 0 ustar 00 HTML.Allowed TYPE: itext/null VERSION: 2.0.0 DEFAULT: NULL --DESCRIPTION-- <p> This is a preferred convenience directive that combines %HTML.AllowedElements and %HTML.AllowedAttributes. Specify elements and attributes that are allowed using: <code>element1[attr1|attr2],element2...</code>. For example, if you would like to only allow paragraphs and links, specify <code>a[href],p</code>. You can specify attributes that apply to all elements using an asterisk, e.g. <code>*[lang]</code>. You can also use newlines instead of commas to separate elements. </p> <p> <strong>Warning</strong>: All of the constraints on the component directives are still enforced. The syntax is a <em>subset</em> of TinyMCE's <code>valid_elements</code> whitelist: directly copy-pasting it here will probably result in broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes are set, this directive has no effect. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt 0000644 00000001037 15111211001 0017033 0 ustar 00 Output.FixInnerHTML TYPE: bool VERSION: 4.3.0 DEFAULT: true --DESCRIPTION-- <p> If true, HTML Purifier will protect against Internet Explorer's mishandling of the <code>innerHTML</code> attribute by appending a space to any attribute that does not contain angled brackets, spaces or quotes, but contains a backtick. This slightly changes the semantics of any given attribute, so if this is unacceptable and you do not use <code>innerHTML</code> on any of your pages, you can turn this directive off. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt 0000644 00000000412 15111211001 0015516 0 ustar 00 HTML.Trusted TYPE: bool VERSION: 2.0.0 DEFAULT: false --DESCRIPTION-- Indicates whether or not the user input is trusted or not. If the input is trusted, a more expansive set of allowed tags and attributes will be used. See also %CSS.Trusted. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt 0000644 00000000336 15111211001 0017144 0 ustar 00 Attr.AllowedClasses TYPE: lookup/null VERSION: 4.0.0 DEFAULT: null --DESCRIPTION-- List of allowed class values in the class attribute. By default, this is null, which means all classes are allowed. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt 0000644 00000000455 15111211001 0016662 0 ustar 00 AutoFormat.Custom TYPE: list VERSION: 2.0.1 DEFAULT: array() --DESCRIPTION-- <p> This directive can be used to add custom auto-format injectors. Specify an array of injector names (class name minus the prefix) or concrete implementations. Injector class must exist. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt 0000644 00000000616 15111211001 0016074 0 ustar 00 HTML.SafeIframe TYPE: bool VERSION: 4.4.0 DEFAULT: false --DESCRIPTION-- <p> Whether or not to permit iframe tags in untrusted documents. This directive must be accompanied by a whitelist of permitted iframes, such as %URI.SafeIframeRegexp, otherwise it will fatally error. This directive has no effect on strict doctypes, as iframes are not valid. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt 0000644 00000000446 15111211001 0017131 0 ustar 00 Attr.DefaultTextDir TYPE: string DEFAULT: 'ltr' --DESCRIPTION-- Defines the default text direction (ltr or rtl) of the document being parsed. This generally is the same as the value of the dir attribute in HTML, or ltr if that is not specified. --ALLOWED-- 'ltr', 'rtl' --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt 0000644 00000001524 15111211002 0020035 0 ustar 00 HTML.ForbiddenAttributes TYPE: lookup VERSION: 3.1.0 DEFAULT: array() --DESCRIPTION-- <p> While this directive is similar to %HTML.AllowedAttributes, for forwards-compatibility with XML, this attribute has a different syntax. Instead of <code>tag.attr</code>, use <code>tag@attr</code>. To disallow <code>href</code> attributes in <code>a</code> tags, set this directive to <code>a@href</code>. You can also disallow an attribute globally with <code>attr</code> or <code>*@attr</code> (either syntax is fine; the latter is provided for consistency with %HTML.AllowedAttributes). </p> <p> <strong>Warning:</strong> This directive complements %HTML.ForbiddenElements, accordingly, check out that directive for a discussion of why you should think twice before using this directive. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt 0000644 00000000516 15111211002 0016412 0 ustar 00 HTML.Proprietary TYPE: bool VERSION: 3.1.0 DEFAULT: false --DESCRIPTION-- <p> Whether or not to allow proprietary elements and attributes in your documents, as per <code>HTMLPurifier_HTMLModule_Proprietary</code>. <strong>Warning:</strong> This can cause your documents to stop validating! </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt 0000644 00000000305 15111211002 0016573 0 ustar 00 Test.ForceNoIconv TYPE: bool DEFAULT: false --DESCRIPTION-- When set to true, HTMLPurifier_Encoder will act as if iconv does not exist and use only pure PHP implementations. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt 0000644 00000000731 15111211002 0015500 0 ustar 00 HTML.Doctype TYPE: string/null DEFAULT: NULL --DESCRIPTION-- Doctype to use during filtering. Technically speaking this is not actually a doctype (as it does not identify a corresponding DTD), but we are using this name for sake of simplicity. When non-blank, this will override any older directives like %HTML.XHTML or %HTML.Strict. --ALLOWED-- 'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.Language.txt 0000644 00000000444 15111211002 0015741 0 ustar 00 Core.Language TYPE: string VERSION: 2.0.0 DEFAULT: 'en' --DESCRIPTION-- ISO 639 language code for localizable things in HTML Purifier to use, which is mainly error reporting. There is currently only an English (en) translation, so this directive is currently useless. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt 0000644 00000002557 15111211002 0020114 0 ustar 00 Core.LegacyEntityDecoder TYPE: bool VERSION: 4.9.0 DEFAULT: false --DESCRIPTION-- <p> Prior to HTML Purifier 4.9.0, entities were decoded by performing a global search replace for all entities whose decoded versions did not have special meanings under HTML, and replaced them with their decoded versions. We would match all entities, even if they did not have a trailing semicolon, but only if there weren't any trailing alphanumeric characters. </p> <table> <tr><th>Original</th><th>Text</th><th>Attribute</th></tr> <tr><td>&yen;</td><td>¥</td><td>¥</td></tr> <tr><td>&yen</td><td>¥</td><td>¥</td></tr> <tr><td>&yena</td><td>&yena</td><td>&yena</td></tr> <tr><td>&yen=</td><td>¥=</td><td>¥=</td></tr> </table> <p> In HTML Purifier 4.9.0, we changed the behavior of entity parsing to match entities that had missing trailing semicolons in less cases, to more closely match HTML5 parsing behavior: </p> <table> <tr><th>Original</th><th>Text</th><th>Attribute</th></tr> <tr><td>&yen;</td><td>¥</td><td>¥</td></tr> <tr><td>&yen</td><td>¥</td><td>¥</td></tr> <tr><td>&yena</td><td>¥a</td><td>&yena</td></tr> <tr><td>&yen=</td><td>¥=</td><td>&yen=</td></tr> </table> <p> This flag reverts back to pre-HTML Purifier 4.9.0 behavior. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt 0000644 00000000701 15111211002 0021736 0 ustar 00 AutoFormat.RemoveEmpty.RemoveNbsp TYPE: bool VERSION: 4.0.0 DEFAULT: false --DESCRIPTION-- <p> When enabled, HTML Purifier will treat any elements that contain only non-breaking spaces as well as regular whitespace as empty, and remove them when %AutoFormat.RemoveEmpty is enabled. </p> <p> See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements that don't have this behavior applied to them. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt 0000644 00000001226 15111211002 0017630 0 ustar 00 Core.AggressivelyFixLt TYPE: bool VERSION: 2.1.0 DEFAULT: true --DESCRIPTION-- <p> This directive enables aggressive pre-filter fixes HTML Purifier can perform in order to ensure that open angled-brackets do not get killed during parsing stage. Enabling this will result in two preg_replace_callback calls and at least two preg_replace calls for every HTML document parsed; if your users make very well-formed HTML, you can set this directive false. This has no effect when DirectLex is used. </p> <p> <strong>Notice:</strong> This directive's default turned from false to true in HTML Purifier 3.2.0. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt 0000644 00000000506 15111211003 0021611 0 ustar 00 AutoFormat.PurifierLinkify.DocURL TYPE: string VERSION: 2.0.1 DEFAULT: '#%s' ALIASES: AutoFormatParam.PurifierLinkifyDocURL --DESCRIPTION-- <p> Location of configuration documentation to link to, let %s substitute into the configuration's namespace and directive names sans the percent sign. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt 0000644 00000000475 15111211003 0020217 0 ustar 00 AutoFormat.DisplayLinkURI TYPE: bool VERSION: 3.2.0 DEFAULT: false --DESCRIPTION-- <p> This directive turns on the in-text display of URIs in <a> tags, and disables those links. For example, <a href="http://example.com">example</a> becomes example (<a>http://example.com</a>). </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt 0000644 00000001362 15111211003 0017464 0 ustar 00 HTML.ForbiddenElements TYPE: lookup VERSION: 3.1.0 DEFAULT: array() --DESCRIPTION-- <p> This was, perhaps, the most requested feature ever in HTML Purifier. Please don't abuse it! This is the logical inverse of %HTML.AllowedElements, and it will override that directive, or any other directive. </p> <p> If possible, %HTML.Allowed is recommended over this directive, because it can sometimes be difficult to tell whether or not you've forbidden all of the behavior you would like to disallow. If you forbid <code>img</code> with the expectation of preventing images on your site, you'll be in for a nasty surprise when people start using the <code>background-image</code> CSS property. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt 0000644 00000000253 15111211003 0016160 0 ustar 00 HTML.TidyRemove TYPE: lookup VERSION: 2.0.0 DEFAULT: array() --DESCRIPTION-- Fixes to remove from the default set of Tidy fixes as per your level. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.Disable.txt 0000644 00000000420 15111211003 0015323 0 ustar 00 URI.Disable TYPE: bool VERSION: 1.3.0 DEFAULT: false --DESCRIPTION-- <p> Disables all URIs in all forms. Not sure why you'd want to do that (after all, the Internet's founded on the notion of a hyperlink). </p> --ALIASES-- Attr.DisableURI --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt 0000644 00000000506 15111211003 0020364 0 ustar 00 URI.OverrideAllowedSchemes TYPE: bool DEFAULT: true --DESCRIPTION-- If this is set to true (which it is by default), you can override %URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the registry. If false, you will also have to update that directive in order to add more schemes. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt 0000644 00000001205 15111211003 0015640 0 ustar 00 Attr.EnableID TYPE: bool DEFAULT: false VERSION: 1.2.0 --DESCRIPTION-- Allows the ID attribute in HTML. This is disabled by default due to the fact that without proper configuration user input can easily break the validation of a webpage by specifying an ID that is already on the surrounding HTML. If you don't mind throwing caution to the wind, enable this directive, but I strongly recommend you also consider blacklisting IDs you use (%Attr.IDBlacklist) or prefixing all user supplied IDs (%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of pre-1.2.0 versions. --ALIASES-- HTML.EnableAttrID --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt 0000644 00000000362 15111211003 0016272 0 ustar 00 URI.DefinitionID TYPE: string/null VERSION: 2.1.0 DEFAULT: NULL --DESCRIPTION-- <p> Unique identifier for a custom-built URI definition. If you want to add custom URIFilters, you must specify this value. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt 0000644 00000000557 15111211003 0017175 0 ustar 00 HTML.AllowedComments TYPE: lookup VERSION: 4.4.0 DEFAULT: array() --DESCRIPTION-- A whitelist which indicates what explicit comment bodies should be allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp (these directives are union'ed together, so a comment is considered valid if any directive deems it valid.) --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt 0000644 00000001054 15111211004 0020122 0 ustar 00 Core.MaintainLineNumbers TYPE: bool/null VERSION: 2.0.0 DEFAULT: NULL --DESCRIPTION-- <p> If true, HTML Purifier will add line number information to all tokens. This is useful when error reporting is turned on, but can result in significant performance degradation and should not be used when unnecessary. This directive must be used with the DirectLex lexer, as the DOMLex lexer does not (yet) support this functionality. If the value is null, an appropriate value will be selected based on other configuration. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt 0000644 00000000615 15111211004 0015366 0 ustar 00 Attr.ID.HTML5 TYPE: bool/null DEFAULT: null VERSION: 4.8.0 --DESCRIPTION-- In HTML5, restrictions on the format of the id attribute have been significantly relaxed, such that any string is valid so long as it contains no spaces and is at least one character. In lieu of a general HTML5 compatibility flag, set this configuration directive to true to use the relaxed rules. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/info.ini 0000644 00000000055 15111211004 0014222 0 ustar 00 name = "HTML Purifier" ; vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt 0000644 00000001102 15111211004 0020126 0 ustar 00 Attr.AllowedFrameTargets TYPE: lookup DEFAULT: array() --DESCRIPTION-- Lookup table of all allowed link frame targets. Some commonly used link targets include _blank, _self, _parent and _top. Values should be lowercase, as validation will be done in a case-sensitive manner despite W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute so this directive will have no effect in that doctype. XHTML 1.1 does not enable the Target module by default, you will have to manually enable it (see the module documentation for more details.) --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt 0000644 00000000767 15111211004 0020410 0 ustar 00 Core.EscapeInvalidChildren TYPE: bool DEFAULT: false --DESCRIPTION-- <p><strong>Warning:</strong> this configuration option is no longer does anything as of 4.6.0.</p> <p>When true, a child is found that is not allowed in the context of the parent element will be transformed into text as if it were ASCII. When false, that element and all internal tags will be dropped, though text will be preserved. There is no option for dropping the element but preserving child nodes.</p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt 0000644 00000000661 15111211004 0017003 0 ustar 00 Core.CollectErrors TYPE: bool VERSION: 2.0.0 DEFAULT: false --DESCRIPTION-- Whether or not to collect errors found while filtering the document. This is a useful way to give feedback to your users. <strong>Warning:</strong> Currently this feature is very patchy and experimental, with lots of possible error messages not yet implemented. It will not cause any problems, but it may not help your users either. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt 0000644 00000000377 15111211004 0016313 0 ustar 00 Attr.AllowedRev TYPE: lookup VERSION: 1.6.0 DEFAULT: array() --DESCRIPTION-- List of allowed reverse document relationships in the rev attribute. This attribute is a bit of an edge-case; if you don't know what it is for, stay away. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt 0000644 00000001073 15111211005 0016467 0 ustar 00 HTML.BlockWrapper TYPE: string VERSION: 1.3.0 DEFAULT: 'p' --DESCRIPTION-- <p> String name of element to wrap inline elements that are inside a block context. This only occurs in the children of blockquote in strict mode. </p> <p> Example: by default value, <code><blockquote>Foo</blockquote></code> would become <code><blockquote><p>Foo</p></blockquote></code>. The <code><p></code> tags can be replaced with whatever you desire, as long as it is a block level element. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt 0000644 00000000673 15111211005 0017042 0 ustar 00 URI.DisableExternal TYPE: bool VERSION: 1.2.0 DEFAULT: false --DESCRIPTION-- Disables links to external websites. This is a highly effective anti-spam and anti-pagerank-leech measure, but comes at a hefty price: nolinks or images outside of your domain will be allowed. Non-linkified URIs will still be preserved. If you want to be able to link to subdomains or use absolute URIs, specify %URI.Host for your website. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt 0000644 00000000624 15111211005 0016501 0 ustar 00 URI.DefaultScheme TYPE: string/null DEFAULT: 'http' --DESCRIPTION-- <p> Defines through what scheme the output will be served, in order to select the proper object validator when no scheme information is present. </p> <p> Starting with HTML Purifier 4.9.0, the default scheme can be null, in which case we reject all URIs which do not have explicit schemes. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt 0000644 00000002162 15111211005 0020147 0 ustar 00 AutoFormat.AutoParagraph TYPE: bool VERSION: 2.0.1 DEFAULT: false --DESCRIPTION-- <p> This directive turns on auto-paragraphing, where double newlines are converted in to paragraphs whenever possible. Auto-paragraphing: </p> <ul> <li>Always applies to inline elements or text in the root node,</li> <li>Applies to inline elements or text with double newlines in nodes that allow paragraph tags,</li> <li>Applies to double newlines in paragraph tags</li> </ul> <p> <code>p</code> tags must be allowed for this directive to take effect. We do not use <code>br</code> tags for paragraphing, as that is semantically incorrect. </p> <p> To prevent auto-paragraphing as a content-producer, refrain from using double-newlines except to specify a new paragraph or in contexts where it has special meaning (whitespace usually has no meaning except in tags like <code>pre</code>, so this should not be difficult.) To prevent the paragraphing of inline text adjacent to block elements, wrap them in <code>div</code> tags (the behavior is slightly different outside of the root node.) </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt 0000644 00000000173 15111211005 0016367 0 ustar 00 Attr.IDBlacklist TYPE: list DEFAULT: array() DESCRIPTION: Array of IDs not allowed in the document. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt 0000644 00000000512 15111211005 0017256 0 ustar 00 Cache.SerializerPath TYPE: string/null VERSION: 2.0.0 DEFAULT: NULL --DESCRIPTION-- <p> Absolute path with no trailing slash to store serialized definitions in. Default is within the HTML Purifier library inside DefinitionCache/Serializer. This path must be writable by the webserver. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt 0000644 00000000752 15111211005 0016423 0 ustar 00 HTML.MaxImgLength TYPE: int/null DEFAULT: 1200 VERSION: 3.1.1 --DESCRIPTION-- <p> This directive controls the maximum number of pixels in the width and height attributes in <code>img</code> tags. This is in place to prevent imagecrash attacks, disable with null at your own risk. This directive is similar to %CSS.MaxImgLength, and both should be concurrently edited, although there are subtle differences in the input format (the HTML max is an integer). </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt 0000644 00000000455 15111211006 0020545 0 ustar 00 Attr.DefaultInvalidImageAlt TYPE: string DEFAULT: 'Invalid image' --DESCRIPTION-- This is the content of the alt tag of an invalid image if the user had not previously specified an alt attribute. It has no effect when the image is valid but there was no alt attribute present. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt 0000644 00000000671 15111211006 0016105 0 ustar 00 HTML.SafeObject TYPE: bool VERSION: 3.1.1 DEFAULT: false --DESCRIPTION-- <p> Whether or not to permit object tags in documents, with a number of extra security features added to prevent script execution. This is similar to what websites like MySpace do to object tags. You should also enable %Output.FlashCompat in order to generate Internet Explorer compatibility code for your object tags. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt 0000644 00000000661 15111211006 0021331 0 ustar 00 Core.ConvertDocumentToFragment TYPE: bool DEFAULT: true --DESCRIPTION-- This parameter determines whether or not the filter should convert input that is a full document with html and body tags to a fragment of just the contents of a body tag. This parameter is simply something HTML Purifier can do during an edge-case: for most inputs, this processing is not necessary. --ALIASES-- Core.AcceptFullDocuments --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt 0000644 00000000712 15111211006 0017260 0 ustar 00 Core.DisableExcludes TYPE: bool DEFAULT: false VERSION: 4.5.0 --DESCRIPTION-- <p> This directive disables SGML-style exclusions, e.g. the exclusion of <code><object></code> in any descendant of a <code><pre></code> tag. Disabling excludes will allow some invalid documents to pass through HTML Purifier, but HTML Purifier will also be less likely to accidentally remove large documents during processing. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt 0000644 00000001025 15111211006 0016637 0 ustar 00 HTML.DefinitionRev TYPE: int VERSION: 2.0.0 DEFAULT: 1 --DESCRIPTION-- <p> Revision identifier for your custom definition specified in %HTML.DefinitionID. This serves the same purpose: uniquely identifying your custom definition, but this one does so in a chronological context: revision 3 is more up-to-date then revision 2. Thus, when this gets incremented, the cache handling is smart enough to clean up any older revisions of your definition as well as flush the cache. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.Base.txt 0000644 00000001216 15111211006 0014641 0 ustar 00 URI.Base TYPE: string/null VERSION: 2.1.0 DEFAULT: NULL --DESCRIPTION-- <p> The base URI is the URI of the document this purified HTML will be inserted into. This information is important if HTML Purifier needs to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute is on. You may use a non-absolute URI for this value, but behavior may vary (%URI.MakeAbsolute deals nicely with both absolute and relative paths, but forwards-compatibility is not guaranteed). <strong>Warning:</strong> If set, the scheme on this URI overrides the one specified by %URI.DefaultScheme. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt 0000644 00000001562 15111211007 0017165 0 ustar 00 HTML.AllowedElements TYPE: lookup/null VERSION: 1.3.0 DEFAULT: NULL --DESCRIPTION-- <p> If HTML Purifier's tag set is unsatisfactory for your needs, you can overload it with your own list of tags to allow. If you change this, you probably also want to change %HTML.AllowedAttributes; see also %HTML.Allowed which lets you set allowed elements and attributes at the same time. </p> <p> If you attempt to allow an element that HTML Purifier does not know about, HTML Purifier will raise an error. You will need to manually tell HTML Purifier about this element by using the <a href="http://htmlpurifier.org/docs/enduser-customize.html">advanced customization features.</a> </p> <p> <strong>Warning:</strong> If another directive conflicts with the elements here, <em>that</em> directive will win and override. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt 0000644 00000000304 15111211007 0016523 0 ustar 00 CSS.DefinitionRev TYPE: int VERSION: 2.0.0 DEFAULT: 1 --DESCRIPTION-- <p> Revision identifier for your custom definition. See %HTML.DefinitionRev for details. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt 0000644 00000000423 15111211007 0017054 0 ustar 00 CSS.AllowDuplicates TYPE: bool DEFAULT: false VERSION: 4.8.0 --DESCRIPTION-- <p> By default, HTML Purifier removes duplicate CSS properties, like <code>color:red; color:blue</code>. If this is set to true, duplicate properties are allowed. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt 0000644 00000000653 15111211007 0017232 0 ustar 00 URI.DisableResources TYPE: bool VERSION: 4.2.0 DEFAULT: false --DESCRIPTION-- <p> Disables embedding resources, essentially meaning no pictures. You can still link to them though. See %URI.DisableExternalResources for why this might be a good idea. </p> <p> <em>Note:</em> While this directive has been available since 1.3.0, it didn't actually start doing anything until 4.2.0. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt 0000644 00000001157 15111211007 0016311 0 ustar 00 CSS.MaxImgLength TYPE: string/null DEFAULT: '1200px' VERSION: 3.1.1 --DESCRIPTION-- <p> This parameter sets the maximum allowed length on <code>img</code> tags, effectively the <code>width</code> and <code>height</code> properties. Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is in place to prevent imagecrash attacks, disable with null at your own risk. This directive is similar to %HTML.MaxImgLength, and both should be concurrently edited, although there are subtle differences in the input format (the CSS max is a number with a unit). </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt 0000644 00000000743 15111211007 0015714 0 ustar 00 HTML.SafeEmbed TYPE: bool VERSION: 3.1.1 DEFAULT: false --DESCRIPTION-- <p> Whether or not to permit embed tags in documents, with a number of extra security features added to prevent script execution. This is similar to what websites like MySpace do to embed tags. Embed is a proprietary element and will cause your website to stop validating; you should see if you can use %Output.FlashCompat with %HTML.SafeObject instead first.</p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt 0000644 00000001224 15111211010 0016661 0 ustar 00 Attr.IDPrefixLocal TYPE: string VERSION: 1.2.0 DEFAULT: '' --DESCRIPTION-- Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you need to allow multiple sets of user content on web page, you may need to have a seperate prefix that changes with each iteration. This way, seperately submitted user content displayed on the same page doesn't clobber each other. Ideal values are unique identifiers for the content it represents (i.e. the id of the row in the database). Be sure to add a seperator (like an underscore) at the end. Warning: this directive will not work unless %Attr.IDPrefix is set to a non-empty value! --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt 0000644 00000000727 15111211010 0017156 0 ustar 00 HTML.Attr.Name.UseCDATA TYPE: bool DEFAULT: false VERSION: 4.0.0 --DESCRIPTION-- The W3C specification DTD defines the name attribute to be CDATA, not ID, due to limitations of DTD. In certain documents, this relaxed behavior is desired, whether it is to specify duplicate names, or to specify names that would be illegal IDs (for example, names that begin with a digit.) Set this configuration directive to true to use the relaxed parsing rules. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt 0000644 00000000737 15111211010 0017234 0 ustar 00 Attr.DefaultImageAlt TYPE: string/null DEFAULT: null VERSION: 3.2.0 --DESCRIPTION-- This is the content of the alt tag of an image if the user had not previously specified an alt attribute. This applies to all images without a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which only applies to invalid images, and overrides in the case of an invalid image. Default behavior with null is to use the basename of the src tag for the alt. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt 0000644 00000000427 15111211010 0017663 0 ustar 00 Core.NormalizeNewlines TYPE: bool VERSION: 4.2.0 DEFAULT: true --DESCRIPTION-- <p> Whether or not to normalize newlines to the operating system default. When <code>false</code>, HTML Purifier will attempt to preserve mixed newline files. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt 0000644 00000000457 15111211010 0016043 0 ustar 00 Core.EnableIDNA TYPE: bool DEFAULT: false VERSION: 4.4.0 --DESCRIPTION-- Allows international domain names in URLs. This configuration option requires the PEAR Net_IDNA2 module to be installed. It operates by punycoding any internationalized host names for maximum portability. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt 0000644 00000001722 15111211010 0016656 0 ustar 00 URI.MungeSecretKey TYPE: string/null VERSION: 3.1.1 DEFAULT: NULL --DESCRIPTION-- <p> This directive enables secure checksum generation along with %URI.Munge. It should be set to a secure key that is not shared with anyone else. The checksum can be placed in the URI using %t. Use of this checksum affords an additional level of protection by allowing a redirector to check if a URI has passed through HTML Purifier with this line: </p> <pre>$checksum === hash_hmac("sha256", $url, $secret_key)</pre> <p> If the output is TRUE, the redirector script should accept the URI. </p> <p> Please note that it would still be possible for an attacker to procure secure hashes en-mass by abusing your website's Preview feature or the like, but this service affords an additional level of protection that should be combined with website blacklisting. </p> <p> Remember this has no effect if %URI.Munge is not on. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt 0000644 00000001473 15111211011 0016713 0 ustar 00 Output.TidyFormat TYPE: bool VERSION: 1.1.1 DEFAULT: false --DESCRIPTION-- <p> Determines whether or not to run Tidy on the final output for pretty formatting reasons, such as indentation and wrap. </p> <p> This can greatly improve readability for editors who are hand-editing the HTML, but is by no means necessary as HTML Purifier has already fixed all major errors the HTML may have had. Tidy is a non-default extension, and this directive will silently fail if Tidy is not available. </p> <p> If you are looking to make the overall look of your page's source better, I recommend running Tidy on the entire page rather than just user-content (after all, the indentation relative to the containing blocks will be incorrect). </p> --ALIASES-- Core.TidyFormat --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt 0000644 00000000627 15111211011 0020701 0 ustar 00 Cache.SerializerPermissions TYPE: int/null VERSION: 4.3.0 DEFAULT: 0755 --DESCRIPTION-- <p> Directory permissions of the files and directories created inside the DefinitionCache/Serializer or other custom serializer path. </p> <p> In HTML Purifier 4.8.0, this also supports <code>NULL</code>, which means that no chmod'ing or directory creation shall occur. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt 0000644 00000001045 15111211011 0020724 0 ustar 00 URI.DisableExternalResources TYPE: bool VERSION: 1.3.0 DEFAULT: false --DESCRIPTION-- Disables the embedding of external resources, preventing users from embedding things like images from other hosts. This prevents access tracking (good for email viewers), bandwidth leeching, cross-site request forging, goatse.cx posting, and other nasties, but also results in a loss of end-user functionality (they can't directly post a pic they posted from Flickr anymore). Use it if you don't have a robust user-content moderation team. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt 0000644 00000003356 15111211011 0017670 0 ustar 00 AutoFormat.RemoveEmpty TYPE: bool VERSION: 3.2.0 DEFAULT: false --DESCRIPTION-- <p> When enabled, HTML Purifier will attempt to remove empty elements that contribute no semantic information to the document. The following types of nodes will be removed: </p> <ul><li> Tags with no attributes and no content, and that are not empty elements (remove <code><a></a></code> but not <code><br /></code>), and </li> <li> Tags with no content, except for:<ul> <li>The <code>colgroup</code> element, or</li> <li> Elements with the <code>id</code> or <code>name</code> attribute, when those attributes are permitted on those elements. </li> </ul></li> </ul> <p> Please be very careful when using this functionality; while it may not seem that empty elements contain useful information, they can alter the layout of a document given appropriate styling. This directive is most useful when you are processing machine-generated HTML, please avoid using it on regular user HTML. </p> <p> Elements that contain only whitespace will be treated as empty. Non-breaking spaces, however, do not count as whitespace. See %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. </p> <p> This algorithm is not perfect; you may still notice some empty tags, particularly if a node had elements, but those elements were later removed because they were not permitted in that context, or tags that, after being auto-closed by another tag, where empty. This is for safety reasons to prevent clever code from breaking validation. The general rule of thumb: if a tag looked empty on the way in, it will get removed; if HTML Purifier made it empty, it will stay. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt 0000644 00000000715 15111211011 0016223 0 ustar 00 CSS.AllowTricky TYPE: bool DEFAULT: false VERSION: 3.1.0 --DESCRIPTION-- This parameter determines whether or not to allow "tricky" CSS properties and values. Tricky CSS properties/values can drastically modify page layout or be used for deceptive practices but do not directly constitute a security risk. For example, <code>display:none;</code> is considered a tricky property that will only be allowed if this directive is set to true. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt 0000644 00000000447 15111211011 0023307 0 ustar 00 AutoFormat.RemoveSpansWithoutAttributes TYPE: bool VERSION: 4.0.1 DEFAULT: false --DESCRIPTION-- <p> This directive causes <code>span</code> tags without any attributes to be removed. It will also remove spans that had all attributes removed during processing. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt 0000644 00000000554 15111211012 0024065 0 ustar 00 AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions TYPE: lookup VERSION: 4.0.0 DEFAULT: array('td' => true, 'th' => true) --DESCRIPTION-- <p> When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp are enabled, this directive defines what HTML elements should not be removede if they have only a non-breaking space in them. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt 0000644 00000002272 15111211012 0016401 0 ustar 00 HTML.DefinitionID TYPE: string/null DEFAULT: NULL VERSION: 2.0.0 --DESCRIPTION-- <p> Unique identifier for a custom-built HTML definition. If you edit the raw version of the HTMLDefinition, introducing changes that the configuration object does not reflect, you must specify this variable. If you change your custom edits, you should change this directive, or clear your cache. Example: </p> <pre> $config = HTMLPurifier_Config::createDefault(); $config->set('HTML', 'DefinitionID', '1'); $def = $config->getHTMLDefinition(); $def->addAttribute('a', 'tabindex', 'Number'); </pre> <p> In the above example, the configuration is still at the defaults, but using the advanced API, an extra attribute has been added. The configuration object normally has no way of knowing that this change has taken place, so it needs an extra directive: %HTML.DefinitionID. If someone else attempts to use the default configuration, these two pieces of code will not clobber each other in the cache, since one has an extra directive attached to it. </p> <p> You <em>must</em> specify a value to this directive to use the advanced API features. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt 0000644 00000000616 15111211012 0022137 0 ustar 00 Core.RemoveProcessingInstructions TYPE: bool VERSION: 4.2.0 DEFAULT: false --DESCRIPTION-- Instead of escaping processing instructions in the form <code><? ... ?></code>, remove it out-right. This may be useful if the HTML you are validating contains XML processing instruction gunk, however, it can also be user-unfriendly for people attempting to post PHP snippets. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt 0000644 00000010512 15111211012 0017022 0 ustar 00 Core.ColorKeywords TYPE: hash VERSION: 2.0.0 --DEFAULT-- array ( 'aliceblue' => '#F0F8FF', 'antiquewhite' => '#FAEBD7', 'aqua' => '#00FFFF', 'aquamarine' => '#7FFFD4', 'azure' => '#F0FFFF', 'beige' => '#F5F5DC', 'bisque' => '#FFE4C4', 'black' => '#000000', 'blanchedalmond' => '#FFEBCD', 'blue' => '#0000FF', 'blueviolet' => '#8A2BE2', 'brown' => '#A52A2A', 'burlywood' => '#DEB887', 'cadetblue' => '#5F9EA0', 'chartreuse' => '#7FFF00', 'chocolate' => '#D2691E', 'coral' => '#FF7F50', 'cornflowerblue' => '#6495ED', 'cornsilk' => '#FFF8DC', 'crimson' => '#DC143C', 'cyan' => '#00FFFF', 'darkblue' => '#00008B', 'darkcyan' => '#008B8B', 'darkgoldenrod' => '#B8860B', 'darkgray' => '#A9A9A9', 'darkgrey' => '#A9A9A9', 'darkgreen' => '#006400', 'darkkhaki' => '#BDB76B', 'darkmagenta' => '#8B008B', 'darkolivegreen' => '#556B2F', 'darkorange' => '#FF8C00', 'darkorchid' => '#9932CC', 'darkred' => '#8B0000', 'darksalmon' => '#E9967A', 'darkseagreen' => '#8FBC8F', 'darkslateblue' => '#483D8B', 'darkslategray' => '#2F4F4F', 'darkslategrey' => '#2F4F4F', 'darkturquoise' => '#00CED1', 'darkviolet' => '#9400D3', 'deeppink' => '#FF1493', 'deepskyblue' => '#00BFFF', 'dimgray' => '#696969', 'dimgrey' => '#696969', 'dodgerblue' => '#1E90FF', 'firebrick' => '#B22222', 'floralwhite' => '#FFFAF0', 'forestgreen' => '#228B22', 'fuchsia' => '#FF00FF', 'gainsboro' => '#DCDCDC', 'ghostwhite' => '#F8F8FF', 'gold' => '#FFD700', 'goldenrod' => '#DAA520', 'gray' => '#808080', 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#ADFF2F', 'honeydew' => '#F0FFF0', 'hotpink' => '#FF69B4', 'indianred' => '#CD5C5C', 'indigo' => '#4B0082', 'ivory' => '#FFFFF0', 'khaki' => '#F0E68C', 'lavender' => '#E6E6FA', 'lavenderblush' => '#FFF0F5', 'lawngreen' => '#7CFC00', 'lemonchiffon' => '#FFFACD', 'lightblue' => '#ADD8E6', 'lightcoral' => '#F08080', 'lightcyan' => '#E0FFFF', 'lightgoldenrodyellow' => '#FAFAD2', 'lightgray' => '#D3D3D3', 'lightgrey' => '#D3D3D3', 'lightgreen' => '#90EE90', 'lightpink' => '#FFB6C1', 'lightsalmon' => '#FFA07A', 'lightseagreen' => '#20B2AA', 'lightskyblue' => '#87CEFA', 'lightslategray' => '#778899', 'lightslategrey' => '#778899', 'lightsteelblue' => '#B0C4DE', 'lightyellow' => '#FFFFE0', 'lime' => '#00FF00', 'limegreen' => '#32CD32', 'linen' => '#FAF0E6', 'magenta' => '#FF00FF', 'maroon' => '#800000', 'mediumaquamarine' => '#66CDAA', 'mediumblue' => '#0000CD', 'mediumorchid' => '#BA55D3', 'mediumpurple' => '#9370DB', 'mediumseagreen' => '#3CB371', 'mediumslateblue' => '#7B68EE', 'mediumspringgreen' => '#00FA9A', 'mediumturquoise' => '#48D1CC', 'mediumvioletred' => '#C71585', 'midnightblue' => '#191970', 'mintcream' => '#F5FFFA', 'mistyrose' => '#FFE4E1', 'moccasin' => '#FFE4B5', 'navajowhite' => '#FFDEAD', 'navy' => '#000080', 'oldlace' => '#FDF5E6', 'olive' => '#808000', 'olivedrab' => '#6B8E23', 'orange' => '#FFA500', 'orangered' => '#FF4500', 'orchid' => '#DA70D6', 'palegoldenrod' => '#EEE8AA', 'palegreen' => '#98FB98', 'paleturquoise' => '#AFEEEE', 'palevioletred' => '#DB7093', 'papayawhip' => '#FFEFD5', 'peachpuff' => '#FFDAB9', 'peru' => '#CD853F', 'pink' => '#FFC0CB', 'plum' => '#DDA0DD', 'powderblue' => '#B0E0E6', 'purple' => '#800080', 'rebeccapurple' => '#663399', 'red' => '#FF0000', 'rosybrown' => '#BC8F8F', 'royalblue' => '#4169E1', 'saddlebrown' => '#8B4513', 'salmon' => '#FA8072', 'sandybrown' => '#F4A460', 'seagreen' => '#2E8B57', 'seashell' => '#FFF5EE', 'sienna' => '#A0522D', 'silver' => '#C0C0C0', 'skyblue' => '#87CEEB', 'slateblue' => '#6A5ACD', 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#FFFAFA', 'springgreen' => '#00FF7F', 'steelblue' => '#4682B4', 'tan' => '#D2B48C', 'teal' => '#008080', 'thistle' => '#D8BFD8', 'tomato' => '#FF6347', 'turquoise' => '#40E0D0', 'violet' => '#EE82EE', 'wheat' => '#F5DEB3', 'white' => '#FFFFFF', 'whitesmoke' => '#F5F5F5', 'yellow' => '#FFFF00', 'yellowgreen' => '#9ACD32' ) --DESCRIPTION-- Lookup array of color names to six digit hexadecimal number corresponding to color, with preceding hash mark. Used when parsing colors. The lookup is done in a case-insensitive manner. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt 0000644 00000001143 15111211012 0016731 0 ustar 00 URI.MungeResources TYPE: bool VERSION: 3.1.1 DEFAULT: false --DESCRIPTION-- <p> If true, any URI munging directives like %URI.Munge will also apply to embedded resources, such as <code><img src=""></code>. Be careful enabling this directive if you have a redirector script that does not use the <code>Location</code> HTTP header; all of your images and other embedded resources will break. </p> <p> <strong>Warning:</strong> It is strongly advised you use this in conjunction %URI.MungeSecretKey to mitigate the security risk of an open redirector. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt 0000644 00000005306 15111211012 0020347 0 ustar 00 Filter.ExtractStyleBlocks TYPE: bool VERSION: 3.1.0 DEFAULT: false EXTERNAL: CSSTidy --DESCRIPTION-- <p> This directive turns on the style block extraction filter, which removes <code>style</code> blocks from input HTML, cleans them up with CSSTidy, and places them in the <code>StyleBlocks</code> context variable, for further use by you, usually to be placed in an external stylesheet, or a <code>style</code> block in the <code>head</code> of your document. </p> <p> Sample usage: </p> <pre><![CDATA[ <?php header('Content-type: text/html; charset=utf-8'); echo '<?xml version="1.0" encoding="UTF-8"?>'; ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <title>Filter.ExtractStyleBlocks</title> <?php require_once '/path/to/library/HTMLPurifier.auto.php'; require_once '/path/to/csstidy.class.php'; $dirty = '<style>body {color:#F00;}</style> Some text'; $config = HTMLPurifier_Config::createDefault(); $config->set('Filter', 'ExtractStyleBlocks', true); $purifier = new HTMLPurifier($config); $html = $purifier->purify($dirty); // This implementation writes the stylesheets to the styles/ directory. // You can also echo the styles inside the document, but it's a bit // more difficult to make sure they get interpreted properly by // browsers; try the usual CSS armoring techniques. $styles = $purifier->context->get('StyleBlocks'); $dir = 'styles/'; if (!is_dir($dir)) mkdir($dir); $hash = sha1($_GET['html']); foreach ($styles as $i => $style) { file_put_contents($name = $dir . $hash . "_$i"); echo '<link rel="stylesheet" type="text/css" href="'.$name.'" />'; } ?> </head> <body> <div> <?php echo $html; ?> </div> </b]]><![CDATA[ody> </html> ]]></pre> <p> <strong>Warning:</strong> It is possible for a user to mount an imagecrash attack using this CSS. Counter-measures are difficult; it is not simply enough to limit the range of CSS lengths (using relative lengths with many nesting levels allows for large values to be attained without actually specifying them in the stylesheet), and the flexible nature of selectors makes it difficult to selectively disable lengths on image tags (HTML Purifier, however, does disable CSS width and height in inline styling). There are probably two effective counter measures: an explicit width and height set to auto in all images in your document (unlikely) or the disabling of width and height (somewhat reasonable). Whether or not these measures should be used is left to the reader. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt 0000644 00000000373 15111211012 0015412 0 ustar 00 CSS.Trusted TYPE: bool VERSION: 4.2.1 DEFAULT: false --DESCRIPTION-- Indicates whether or not the user's CSS input is trusted or not. If the input is trusted, a more expansive set of allowed properties. See also %HTML.Trusted. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt 0000644 00000000472 15111211013 0015326 0 ustar 00 HTML.Parent TYPE: string VERSION: 1.3.0 DEFAULT: 'div' --DESCRIPTION-- <p> String name of element that HTML fragment passed to library will be inserted in. An interesting variation would be using span as the parent element, meaning that only inline tags would be allowed. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt 0000644 00000000243 15111211013 0015413 0 ustar 00 HTML.TidyAdd TYPE: lookup VERSION: 2.0.0 DEFAULT: array() --DESCRIPTION-- Fixes to add to the default set of Tidy fixes as per your level. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt 0000644 00000000244 15111211013 0016276 0 ustar 00 CSS.Proprietary TYPE: bool VERSION: 3.0.0 DEFAULT: false --DESCRIPTION-- <p> Whether or not to allow safe, proprietary CSS values. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt 0000644 00000000437 15111211013 0020115 0 ustar 00 HTML.FlashAllowFullScreen TYPE: bool VERSION: 4.2.0 DEFAULT: false --DESCRIPTION-- <p> Whether or not to permit embedded Flash content from %HTML.SafeObject to expand to the full screen. Corresponds to the <code>allowFullScreen</code> parameter. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt 0000644 00000001636 15111211013 0016525 0 ustar 00 Attr.ClassUseCDATA TYPE: bool/null DEFAULT: null VERSION: 4.0.0 --DESCRIPTION-- If null, class will auto-detect the doctype and, if matching XHTML 1.1 or XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, it will use a relaxed CDATA definition. If true, the relaxed CDATA definition is forced; if false, the NMTOKENS definition is forced. To get behavior of HTML Purifier prior to 4.0.0, set this directive to false. Some rational behind the auto-detection: in previous versions of HTML Purifier, it was assumed that the form of class was NMTOKENS, as specified by the XHTML Modularization (representing XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however specify class as CDATA. HTML 5 effectively defines it as CDATA, but with the additional constraint that each name should be unique (this is not explicitly outlined in previous specifications). --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt 0000644 00000001154 15111211013 0015774 0 ustar 00 HTML.TidyLevel TYPE: string VERSION: 2.0.0 DEFAULT: 'medium' --DESCRIPTION-- <p>General level of cleanliness the Tidy module should enforce. There are four allowed values:</p> <dl> <dt>none</dt> <dd>No extra tidying should be done</dd> <dt>light</dt> <dd>Only fix elements that would be discarded otherwise due to lack of support in doctype</dd> <dt>medium</dt> <dd>Enforce best practices</dd> <dt>heavy</dt> <dd>Transform all deprecated elements and attributes to standards compliant equivalents</dd> </dl> --ALLOWED-- 'none', 'light', 'medium', 'heavy' --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt 0000644 00000000353 15111211014 0014770 0 ustar 00 HTML.XHTML TYPE: bool DEFAULT: true VERSION: 1.1.0 DEPRECATED-VERSION: 1.7.0 DEPRECATED-USE: HTML.Doctype --DESCRIPTION-- Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. --ALIASES-- Core.XHTML --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt 0000644 00000001307 15111211014 0020344 0 ustar 00 HTML.AllowedCommentsRegexp TYPE: string/null VERSION: 4.4.0 DEFAULT: NULL --DESCRIPTION-- A regexp, which if it matches the body of a comment, indicates that it should be allowed. Trailing and leading spaces are removed prior to running this regular expression. <strong>Warning:</strong> Make sure you specify correct anchor metacharacters <code>^regex$</code>, otherwise you may accept comments that you did not mean to! In particular, the regex <code>/foo|bar/</code> is probably not sufficiently strict, since it also allows <code>foobar</code>. See also %HTML.AllowedComments (these directives are union'ed together, so a comment is considered valid if any directive deems it valid.) --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt 0000644 00000000745 15111211014 0016673 0 ustar 00 URI.AllowedSchemes TYPE: lookup --DEFAULT-- array ( 'http' => true, 'https' => true, 'mailto' => true, 'ftp' => true, 'nntp' => true, 'news' => true, 'tel' => true, ) --DESCRIPTION-- Whitelist that defines the schemes that a URI is allowed to have. This prevents XSS attacks from using pseudo-schemes like javascript or mocha. There is also support for the <code>data</code> and <code>file</code> URI schemes, but they are not enabled by default. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt 0000644 00000000463 15111211014 0016031 0 ustar 00 Filter.Custom TYPE: list VERSION: 3.1.0 DEFAULT: array() --DESCRIPTION-- <p> This directive can be used to add custom filters; it is nearly the equivalent of the now deprecated <code>HTMLPurifier->addFilter()</code> method. Specify an array of concrete implementations. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt 0000644 00000000335 15111211014 0015344 0 ustar 00 HTML.Strict TYPE: bool VERSION: 1.3.0 DEFAULT: false DEPRECATED-VERSION: 1.7.0 DEPRECATED-USE: HTML.Doctype --DESCRIPTION-- Determines whether or not to use Transitional (loose) or Strict rulesets. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt 0000644 00000000415 15111211014 0017030 0 ustar 00 Output.FlashCompat TYPE: bool VERSION: 4.1.0 DEFAULT: false --DESCRIPTION-- <p> If true, HTML Purifier will generate Internet Explorer compatibility code for all object code. This is highly recommended if you enable %HTML.SafeObject. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt 0000644 00000000351 15111211015 0016675 0 ustar 00 HTML.CustomDoctype TYPE: string/null VERSION: 2.0.1 DEFAULT: NULL --DESCRIPTION-- A custom doctype for power-users who defined their own document type. This directive only applies when %HTML.Doctype is blank. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt 0000644 00000000775 15111211015 0016423 0 ustar 00 Output.SortAttr TYPE: bool VERSION: 3.2.0 DEFAULT: false --DESCRIPTION-- <p> If true, HTML Purifier will sort attributes by name before writing them back to the document, converting a tag like: <code><el b="" a="" c="" /></code> to <code><el a="" b="" c="" /></code>. This is a workaround for a bug in FCKeditor which causes it to swap attributes order, adding noise to text diffs. If you're not seeing this bug, chances are, you don't need this directive. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt 0000644 00000001077 15111211015 0017115 0 ustar 00 Core.HiddenElements TYPE: lookup --DEFAULT-- array ( 'script' => true, 'style' => true, ) --DESCRIPTION-- <p> This directive is a lookup array of elements which should have their contents removed when they are not allowed by the HTML definition. For example, the contents of a <code>script</code> tag are not normally shown in a document, so if script tags are to be removed, their contents should be removed to. This is opposed to a <code>b</code> tag, which defines some presentational changes but does not hide its contents. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt 0000644 00000000745 15111211015 0022104 0 ustar 00 Filter.ExtractStyleBlocks.Escaping TYPE: bool VERSION: 3.0.0 DEFAULT: true ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping --DESCRIPTION-- <p> Whether or not to escape the dangerous characters <, > and & as \3C, \3E and \26, respectively. This is can be safely set to false if the contents of StyleBlocks will be placed in an external stylesheet, where there is no risk of it being interpreted as HTML. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt 0000644 00000001075 15111211015 0021232 0 ustar 00 Core.AggressivelyRemoveScript TYPE: bool VERSION: 4.9.0 DEFAULT: true --DESCRIPTION-- <p> This directive enables aggressive pre-filter removal of script tags. This is not necessary for security, but it can help work around a bug in libxml where embedded HTML elements inside script sections cause the parser to choke. To revert to pre-4.9.0 behavior, set this to false. This directive has no effect if %Core.Trusted is true, %Core.RemoveScriptContents is false, or %Core.HiddenElements does not contain script. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.Munge.txt 0000644 00000005307 15111211015 0015047 0 ustar 00 URI.Munge TYPE: string/null VERSION: 1.3.0 DEFAULT: NULL --DESCRIPTION-- <p> Munges all browsable (usually http, https and ftp) absolute URIs into another URI, usually a URI redirection service. This directive accepts a URI, formatted with a <code>%s</code> where the url-encoded original URI should be inserted (sample: <code>http://www.google.com/url?q=%s</code>). </p> <p> Uses for this directive: </p> <ul> <li> Prevent PageRank leaks, while being fairly transparent to users (you may also want to add some client side JavaScript to override the text in the statusbar). <strong>Notice</strong>: Many security experts believe that this form of protection does not deter spam-bots. </li> <li> Redirect users to a splash page telling them they are leaving your website. While this is poor usability practice, it is often mandated in corporate environments. </li> </ul> <p> Prior to HTML Purifier 3.1.1, this directive also enabled the munging of browsable external resources, which could break things if your redirection script was a splash page or used <code>meta</code> tags. To revert to previous behavior, please use %URI.MungeResources. </p> <p> You may want to also use %URI.MungeSecretKey along with this directive in order to enforce what URIs your redirector script allows. Open redirector scripts can be a security risk and negatively affect the reputation of your domain name. </p> <p> Starting with HTML Purifier 3.1.1, there is also these substitutions: </p> <table> <thead> <tr> <th>Key</th> <th>Description</th> <th>Example <code><a href=""></code></th> </tr> </thead> <tbody> <tr> <td>%r</td> <td>1 - The URI embeds a resource<br />(blank) - The URI is merely a link</td> <td></td> </tr> <tr> <td>%n</td> <td>The name of the tag this URI came from</td> <td>a</td> </tr> <tr> <td>%m</td> <td>The name of the attribute this URI came from</td> <td>href</td> </tr> <tr> <td>%p</td> <td>The name of the CSS property this URI came from, or blank if irrelevant</td> <td></td> </tr> </tbody> </table> <p> Admittedly, these letters are somewhat arbitrary; the only stipulation was that they couldn't be a through f. r is for resource (I would have preferred e, but you take what you can get), n is for name, m was picked because it came after n (and I couldn't use a), p is for property. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt 0000644 00000001156 15111211016 0016321 0 ustar 00 HTML.CoreModules TYPE: lookup VERSION: 2.0.0 --DEFAULT-- array ( 'Structure' => true, 'Text' => true, 'Hypertext' => true, 'List' => true, 'NonXMLCommonAttributes' => true, 'XMLCommonAttributes' => true, 'CommonAttributes' => true, ) --DESCRIPTION-- <p> Certain modularized doctypes (XHTML, namely), have certain modules that must be included for the doctype to be an conforming document type: put those modules here. By default, XHTML's core modules are used. You can set this to a blank array to disable core module protection, but this is not recommended. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt 0000644 00000000312 15111211016 0017543 0 ustar 00 Core.EscapeInvalidTags TYPE: bool DEFAULT: false --DESCRIPTION-- When true, invalid tags will be written back to the document as plain text. Otherwise, they are silently dropped. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt 0000644 00000001326 15111211016 0017017 0 ustar 00 HTML.AllowedModules TYPE: lookup/null VERSION: 2.0.0 DEFAULT: NULL --DESCRIPTION-- <p> A doctype comes with a set of usual modules to use. Without having to mucking about with the doctypes, you can quickly activate or disable these modules by specifying which modules you wish to allow with this directive. This is most useful for unit testing specific modules, although end users may find it useful for their own ends. </p> <p> If you specify a module that does not exist, the manager will silently fail to use it, so be careful! User-defined modules are not affected by this directive. Modules defined in %HTML.CoreModules are not affected by this directive. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt 0000644 00000000421 15111211016 0016633 0 ustar 00 HTML.SafeScripting TYPE: lookup VERSION: 4.5.0 DEFAULT: array() --DESCRIPTION-- <p> Whether or not to permit script tags to external scripts in documents. Inline scripting is not allowed, and the script must match an explicit whitelist. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt 0000644 00000000436 15111211016 0017360 0 ustar 00 HTML.TargetNoreferrer TYPE: bool VERSION: 4.8.0 DEFAULT: TRUE --DESCRIPTION-- If enabled, noreferrer rel attributes are added to links which have a target attribute associated with them. This prevents malicious destinations from overwriting the original window. --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt 0000644 00000001601 15111211016 0017143 0 ustar 00 URI.SafeIframeRegexp TYPE: string/null VERSION: 4.4.0 DEFAULT: NULL --DESCRIPTION-- <p> A PCRE regular expression that will be matched against an iframe URI. This is a relatively inflexible scheme, but works well enough for the most common use-case of iframes: embedded video. This directive only has an effect if %HTML.SafeIframe is enabled. Here are some example values: </p> <ul> <li><code>%^http://www.youtube.com/embed/%</code> - Allow YouTube videos</li> <li><code>%^http://player.vimeo.com/video/%</code> - Allow Vimeo videos</li> <li><code>%^http://(www.youtube.com/embed/|player.vimeo.com/video/)%</code> - Allow both</li> </ul> <p> Note that this directive does not give you enough granularity to, say, disable all <code>autoplay</code> videos. Pipe up on the HTML Purifier forums if this is a capability you want. </p> --# vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/ValidatorAtom.php 0000644 00000005444 15111211017 0014620 0 ustar 00 <?php /** * Fluent interface for validating the contents of member variables. * This should be immutable. See HTMLPurifier_ConfigSchema_Validator for * use-cases. We name this an 'atom' because it's ONLY for validations that * are independent and usually scalar. */ class HTMLPurifier_ConfigSchema_ValidatorAtom { /** * @type string */ protected $context; /** * @type object */ protected $obj; /** * @type string */ protected $member; /** * @type mixed */ protected $contents; public function __construct($context, $obj, $member) { $this->context = $context; $this->obj = $obj; $this->member = $member; $this->contents =& $obj->$member; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertIsString() { if (!is_string($this->contents)) { $this->error('must be a string'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertIsBool() { if (!is_bool($this->contents)) { $this->error('must be a boolean'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertIsArray() { if (!is_array($this->contents)) { $this->error('must be an array'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertNotNull() { if ($this->contents === null) { $this->error('must not be null'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertAlnum() { $this->assertIsString(); if (!ctype_alnum($this->contents)) { $this->error('must be alphanumeric'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertNotEmpty() { if (empty($this->contents)) { $this->error('must not be empty'); } return $this; } /** * @return HTMLPurifier_ConfigSchema_ValidatorAtom */ public function assertIsLookup() { $this->assertIsArray(); foreach ($this->contents as $v) { if ($v !== true) { $this->error('must be a lookup array'); } } return $this; } /** * @param string $msg * @throws HTMLPurifier_ConfigSchema_Exception */ protected function error($msg) { throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); } } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/Exception.php 0000644 00000000242 15111211017 0013777 0 ustar 00 <?php /** * Exceptions related to configuration schema */ class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception { } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/Interchange/Id.php 0000644 00000002061 15111211017 0014625 0 ustar 00 <?php /** * Represents a directive ID in the interchange format. */ class HTMLPurifier_ConfigSchema_Interchange_Id { /** * @type string */ public $key; /** * @param string $key */ public function __construct($key) { $this->key = $key; } /** * @return string * @warning This is NOT magic, to ensure that people don't abuse SPL and * cause problems for PHP 5.0 support. */ public function toString() { return $this->key; } /** * @return string */ public function getRootNamespace() { return substr($this->key, 0, strpos($this->key, ".")); } /** * @return string */ public function getDirective() { return substr($this->key, strpos($this->key, ".") + 1); } /** * @param string $id * @return HTMLPurifier_ConfigSchema_Interchange_Id */ public static function make($id) { return new HTMLPurifier_ConfigSchema_Interchange_Id($id); } } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/Interchange/Directive.php 0000644 00000003661 15111211017 0016216 0 ustar 00 <?php /** * Interchange component class describing configuration directives. */ class HTMLPurifier_ConfigSchema_Interchange_Directive { /** * ID of directive. * @type HTMLPurifier_ConfigSchema_Interchange_Id */ public $id; /** * Type, e.g. 'integer' or 'istring'. * @type string */ public $type; /** * Default value, e.g. 3 or 'DefaultVal'. * @type mixed */ public $default; /** * HTML description. * @type string */ public $description; /** * Whether or not null is allowed as a value. * @type bool */ public $typeAllowsNull = false; /** * Lookup table of allowed scalar values. * e.g. array('allowed' => true). * Null if all values are allowed. * @type array */ public $allowed; /** * List of aliases for the directive. * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). * @type HTMLPurifier_ConfigSchema_Interchange_Id[] */ public $aliases = array(); /** * Hash of value aliases, e.g. array('alt' => 'real'). Null if value * aliasing is disabled (necessary for non-scalar types). * @type array */ public $valueAliases; /** * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. * Null if the directive has always existed. * @type string */ public $version; /** * ID of directive that supercedes this old directive. * Null if not deprecated. * @type HTMLPurifier_ConfigSchema_Interchange_Id */ public $deprecatedUse; /** * Version of HTML Purifier this directive was deprecated. Null if not * deprecated. * @type string */ public $deprecatedVersion; /** * List of external projects this directive depends on, e.g. array('CSSTidy'). * @type array */ public $external = array(); } // vim: et sw=4 sts=4 HTMLPurifier/ConfigSchema/schema.ser 0000644 00000057176 15111211017 0013325 0 ustar 00 O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:"